All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nasa.worldwind.ogc.kml.KMLLink Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2012 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */

package gov.nasa.worldwind.ogc.kml;

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.event.Message;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.render.DrawContext;
import gov.nasa.worldwind.util.*;
import gov.nasa.worldwind.util.xml.XMLEventParserContext;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import java.awt.*;
import java.net.*;
import java.util.Locale;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Represents the KML Link element and provides access to its contents. The Link maintains a timestamp that
 * indicates when the resource was last updated. The timestamp is updated based on the link's refresh mode. Code that
 * uses a resource loaded from a Link should keep track of when the resource was retrieved, and periodically check that
 * timestamp against the timestamp returned by {@link #getUpdateTime()}.
 *
 * @author tag
 * @version $Id: KMLLink.java 1171 2013-02-11 21:45:02Z dcollins $
 */
public class KMLLink extends KMLAbstractObject
{
    protected static final String DEFAULT_VIEW_FORMAT = "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]";

    /** The time, in milliseconds since the Epoch, at which the linked content was most recently updated. */
    protected AtomicLong updateTime = new AtomicLong();

    /** If this link's href does not need to be modified with a query string, this value is the resource address. */
    protected String finalHref;
    /** The {@link URL} for the raw href. Will be null if the href is not a remote URL or this link has a query string */
    protected URL hrefURL;

    /** Scheduled task that will update the when it runs. Used to implement {@code onInterval} refresh mode. */
    protected ScheduledFuture refreshTask;

    /**
     * Construct an instance.
     *
     * @param namespaceURI the qualifying namespace URI. May be null to indicate no namespace qualification.
     */
    public KMLLink(String namespaceURI)
    {
        super(namespaceURI);
    }

    public String getHref()
    {
        return (String) this.getField("href");
    }

    public String getRefreshMode()
    {
        return (String) this.getField("refreshMode");
    }

    public Double getRefreshInterval()
    {
        return (Double) this.getField("refreshInterval");
    }

    public String getViewRefreshMode()
    {
        return (String) this.getField("viewRefreshMode");
    }

    public Double getViewRefreshTime()
    {
        return (Double) this.getField("viewRefreshTime");
    }

    public Double getViewBoundScale()
    {
        return (Double) this.getField("viewBoundScale");
    }

    public String getViewFormat()
    {
        return (String) this.getField("viewFormat");
    }

    public String getHttpQuery()
    {
        return (String) this.getField("httpQuery");
    }

    /** {@inheritDoc} Overridden to mark {@code onChange} links as updated when a field set. */
    @Override
    public void setField(String keyName, Object value)
    {
        super.setField(keyName, value);

        // If the link refreshes "onChange", mark the link as updated because it has changed.
        if (KMLConstants.ON_CHANGE.equals(this.getRefreshMode()))
        {
            this.setUpdateTime(System.currentTimeMillis());
        }
    }

    /**
     * Returns the time at which the linked resource was last updated. This method is safe to call from any thread.
     *
     * @return The time at which the linked content was most recently updated. See {@link System#currentTimeMillis()}
     *         for its numerical meaning of this timestamp.
     */
    public long getUpdateTime()
    {
        // Schedule a task to refresh the link if the refresh mode requires it. If the client code never calls
        // getUpdateTime, the link may never refresh. But in this case, the link is never rendered, so it doesn't matter.
        // Scheduling a refresh task only when the link is actually used avoids creating a long running update task
        // that may keep the link and it's document in memory even if the application is no longer using the link.
        this.scheduleRefreshIfNeeded();

        return this.updateTime.get();
    }

    /**
     * Specifies the time at which the linked resource was last updated.  This method is safe to call from any thread.
     *
     * @param updateTime The time at which the linked content was most recently updated. See {@link
     *                   System#currentTimeMillis()} for its numerical meaning of this timestamp.
     */
    public void setUpdateTime(long updateTime)
    {
        this.updateTime.set(updateTime);
    }

    /**
     * Specifies the time at which the linked resource expires. If the link's update mode is onExpire, the link will
     * mark itself updated at this time.
     *
     * @param time Time, in milliseconds since the Epoch, at which the link expires. Zero indicates no expiration.
     */
    public void setExpirationTime(long time)
    {
        // If the refresh mode is onExpire, schedule a task to update the link at the expiration time. Otherwise
        // we don't care about the expiration.
        if (KMLConstants.ON_EXPIRE.equals(this.getRefreshMode()))
        {
            // If there is already a task running, cancel it
            if (this.refreshTask != null)
                this.refreshTask.cancel(false);

            if (time != 0)
            {
                long refreshDelay = time - System.currentTimeMillis();
                this.refreshTask = this.scheduleDelayedTask(new RefreshTask(), refreshDelay, TimeUnit.MILLISECONDS);
            }
        }
    }

    /**
     * Schedule a task to refresh the link if the refresh mode requires it. In the case of an {@code onInterval} and
     * {@code onExpire} refresh modes, this method schedules a task to update the link after the refresh interval
     * elapses, but only if such a task has not already been scheduled (only one refresh task is active at a time).
     */
    protected void scheduleRefreshIfNeeded()
    {
        Long refreshTime = this.computeRefreshTime();
        if (refreshTime == null)
            return;

        // Determine if the refresh interval has elapsed since the last refresh task was scheduled.
        boolean intervalElapsed = System.currentTimeMillis() > refreshTime;

        // If the refresh interval has already elapsed then the link needs to refresh immediately.
        if (intervalElapsed)
            this.updateTime.set(System.currentTimeMillis());

        // Schedule a refresh if the refresh interval has elapsed, or if no refresh task is already active.
        // Checking the refresh interval ensures that even if the task fails to run for some reason, a new
        // task will be scheduled after the interval expires.
        if (intervalElapsed || this.refreshTask == null || this.refreshTask.isDone())
        {
            long refreshDelay = refreshTime - System.currentTimeMillis();
            this.refreshTask = this.scheduleDelayedTask(new RefreshTask(), refreshDelay, TimeUnit.MILLISECONDS);
        }
    }

    protected Long computeRefreshTime()
    {
        Long refreshTime = null;

        // Only handle onInterval here. onExpire is handled by KMLNetworkLink when the network resource is retrieved.
        if (KMLConstants.ON_INTERVAL.equals(this.getRefreshMode()))
        {
            Double ri = this.getRefreshInterval();
            refreshTime = ri != null ? this.updateTime.get() + (long) (ri * 1000d) : null;
        }

        if (refreshTime == null)
            return null;

        KMLNetworkLinkControl linkControl = this.getRoot().getNetworkLinkControl();
        if (linkControl != null)
        {
            Long minRefresh = (long) (linkControl.getMinRefreshPeriod() * 1000d);
            if (minRefresh != null && minRefresh > refreshTime)
                refreshTime = minRefresh;
        }

        return refreshTime;
    }

    /**
     * Schedule the link to refresh when an {@link View#VIEW_STOPPED} message is received, and the link's view refresh
     * mode is {@code onStop}.
     *
     * @param msg The message that was received.
     */
    @Override
    public void onMessage(Message msg)
    {
        String viewRefreshMode = this.getViewRefreshMode();
        if (View.VIEW_STOPPED.equals(msg.getName()) && KMLConstants.ON_STOP.equals(viewRefreshMode))
        {
            Double refreshTime = this.getViewRefreshTime();
            if (refreshTime != null)
            {
                this.scheduleDelayedTask(new RefreshTask(), refreshTime.longValue(), TimeUnit.SECONDS);
            }
        }
    }

    /**
     * Schedule a task to mark a link as updated after a delay. The task only executes once.
     *
     * @param task     Task to schedule.
     * @param delay    Delay to wait before executing the task. The time unit is determined by {code timeUnit}.
     * @param timeUnit The time unit of {@code delay}.
     *
     * @return Future that represents the scheduled task.
     */
    protected ScheduledFuture scheduleDelayedTask(Runnable task, long delay, TimeUnit timeUnit)
    {
        return WorldWind.getScheduledTaskService().addScheduledTask(task, delay, timeUnit);
    }

    /** {@inheritDoc} Overridden to set a default refresh mode of {@code onChange} if the refresh mode is not specified. */
    @Override
    public Object parse(XMLEventParserContext ctx, XMLEvent inputEvent, Object... args) throws XMLStreamException
    {
        Object o = super.parse(ctx, inputEvent, args);

        // If the link does not have a refresh mode, set the default refresh mode of "onChange". We set an explicit
        // default after parsing is complete so that we can distinguish links that do not specify a refresh mode from
        // those in which the refresh mode has not yet been parsed.
        if (WWUtil.isEmpty(this.getRefreshMode()))
        {
            this.setField("refreshMode", KMLConstants.ON_CHANGE);
        }

        return o;
    }

    /**
     * Returns the address of the resource specified by this KML link. If the resource specified in this link's
     * href is a local resource, this returns only the href, and ignores the
     * viewFormat and httpQuery. Otherwise, this returns the concatenation of the
     * href, the viewFormat and the httpQuery for form an absolute URL string. If
     * the the href contains a query string, the viewFormat and httpQuery are
     * appended to that string. If necessary, this inserts the & character between the href's
     * query string, the viewFormat, and the httpQuery.
     * 

* This substitutes the following parameters in viewFormat and httpQuery:

    *
  • [bboxWest],[bboxSouth],[bboxEast],[bboxNorth] - visible bounds of the globe, or 0 if the globe * is not visible. The visible bounds are scaled from their centroid by this link's * viewBoundScale.
  • [lookatLon], [lookatLat] - longitude and latitude of the * position on the globe the view is looking at, or 0 if the view is not looking at the globe.
  • *
  • [lookatRange] - distance between view's eye position and the point on the globe the view is * looking at.
  • [lookatTilt], [lookatHeading] - view's tilt and heading.
  • *
  • [lookatTerrainLon], [lookatTerrainLat], [lookatTerrainAlt] - terrain position the view is * looking at, or 0 if the view is not looking at the terrain.
  • [cameraLon], [cameraLat], * [cameraAlt] - view's eye position.
  • [horizFov], [vertFov] - view's horizontal and * vertical field of view.
  • [horizPixels], [vertPixels] - width and height of the * viewport.
  • [terrainEnabled] - always true
  • [clientVersion] * - World Wind client version.
  • [clientName] - World Wind client name.
  • *
  • [kmlVersion] - KML version supported by World Wind.
  • [language] - current * locale's language.
If the viewFormat is unspecified, and the viewRefreshMode * is one of onRequest, onStop or onRegion, this automatically appends the * following information to the query string: BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]. The * [clientName] and [clientVersion] parameters of the httpQuery may be * specified in the configuration file using the keys {@link gov.nasa.worldwind.avlist.AVKey#NAME} and * {@link gov.nasa.worldwind.avlist.AVKey#VERSION}. If not specified, this uses default values of * {@link gov.nasa.worldwind.Version#getVersionName()} and {@link * gov.nasa.worldwind.Version#getVersion()} for [clientName] and [clientVersion], * respectively. * * @param dc the DrawContext used to determine the current view parameters. * * @return the address of the resource specified by this KML link. * * @throws IllegalArgumentException if dc is null. * @see #getHref() * @see #getViewFormat() * @see #getHttpQuery() * @see gov.nasa.worldwind.Configuration */ public String getAddress(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } // See if we've already determined the href is a local reference if (this.finalHref != null) return this.finalHref; String href = this.getHref(); if (href != null) href = href.trim(); if (WWUtil.isEmpty(href)) return href; // If the href is a local resource, the viewFormat and httpQuery parameters are ignored, and we return the href. // We treat the href as a local resource reference if it fails to parse as a URL, or if the URL's protocol is // "file" or "jar". // See OGC KML specification 2.2.0, section 13.1.2. URL url = this.hrefURL != null ? this.hrefURL : WWIO.makeURL(href); if (url == null || this.isLocalReference(url)) { this.finalHref = href; // don't need to parse the href anymore return href; } String queryString = this.buildQueryString(dc); if (WWUtil.isEmpty(queryString)) { this.finalHref = href; // don't need to parse the href anymore return href; } this.hrefURL = url; // retain it so that we don't regenerate it every time try { // Create a new URL, with the full query string. URL newUrl = new URL(this.hrefURL.getProtocol(), this.hrefURL.getHost(), this.hrefURL.getPort(), this.hrefURL.getPath() + queryString); return newUrl.toString(); } catch (MalformedURLException e) { return href; // If constructing a URL from the href and query string fails, assume this is a local file. } } /** * Returns whether the resource specified by the url is a local resource. * * @param url the URL to test. * * @return true if the url specifies a local resource, otherwise false. */ protected boolean isLocalReference(URL url) { return url.getProtocol() == null || "file".equals(url.getProtocol()) || "jar".equals(url.getProtocol()); } /** * This returns the concatenation of the query part of href (if any), the viewFormat and * the httpQuery to form the link URL's query part. This returns null if this link's * href does not specify a URL. This substitutes parameters in viewFormat according to the * specified DrawContext's current viewing parameters, and substitutes parameters in * httpQuery according to the current {@link gov.nasa.worldwind.Configuration} * parameters. * * @param dc the DrawContext used to determine the current view parameters. * * @return the query part of this KML link's address, or null if this link does not specify a URL. */ protected String buildQueryString(DrawContext dc) { URL url = WWIO.makeURL(this.getHref()); if (url == null) return null; StringBuilder queryString = new StringBuilder(url.getQuery() != null ? url.getQuery() : ""); String viewRefreshMode = this.getViewRefreshMode(); if (viewRefreshMode != null) viewRefreshMode = viewRefreshMode.trim(); // Ignore the viewFormat if the viewRefreshMode is unspecified or if the viewRefreshMode is "never". // See OGC KML specification 2.2.0, section 16.22.1. if (!WWUtil.isEmpty(viewRefreshMode) && !KMLConstants.NEVER.equals(viewRefreshMode)) { String s = this.getViewFormat(); if (s != null) s = s.trim(); // Use a default viewFormat that includes the view bounding box parameters if no viewFormat is specified // and the viewRefreshMode is "onStop". // See Google KML Reference: http://code.google.com/apis/kml/documentation/kmlreference.html#link if (s == null && KMLConstants.ON_STOP.equals(viewRefreshMode)) s = DEFAULT_VIEW_FORMAT; // Ignore the viewFormat if it's specified but empty. if (!WWUtil.isEmpty(s)) { Sector viewBounds = this.computeVisibleBounds(dc); //noinspection ConstantConditions s = s.replaceAll("\\[bboxWest\\]", Double.toString(viewBounds.getMinLongitude().degrees)); s = s.replaceAll("\\[bboxSouth\\]", Double.toString(viewBounds.getMinLatitude().degrees)); s = s.replaceAll("\\[bboxEast\\]", Double.toString(viewBounds.getMaxLongitude().degrees)); s = s.replaceAll("\\[bboxNorth\\]", Double.toString(viewBounds.getMaxLatitude().degrees)); View view = dc.getView(); Vec4 centerPoint = view.getCenterPoint(); if (centerPoint != null) { // Use the view's center position as the "look at" position. Position centerPosition = view.getGlobe().computePositionFromPoint(centerPoint); s = s.replaceAll("\\[lookatLat\\]", Double.toString(centerPosition.getLatitude().degrees)); s = s.replaceAll("\\[lookatLon\\]", Double.toString(centerPosition.getLongitude().degrees)); s = s.replaceAll("\\[lookatAlt\\]", Double.toString(centerPosition.getAltitude())); double range = centerPoint.distanceTo3(view.getEyePoint()); s = s.replaceAll("\\[lookatRange\\]", Double.toString(range)); s = s.replaceAll("\\[lookatHeading\\]", Double.toString(view.getHeading().degrees)); s = s.replaceAll("\\[lookatTilt\\]", Double.toString(view.getPitch().degrees)); // TODO make sure that these terrain fields really should be treated the same as the fields above s = s.replaceAll("\\[lookatTerrainLat\\]", Double.toString(centerPosition.getLatitude().degrees)); s = s.replaceAll("\\[lookatTerrainLon\\]", Double.toString(centerPosition.getLongitude().degrees)); s = s.replaceAll("\\[lookatTerrainAlt\\]", Double.toString(centerPosition.getAltitude())); } Position eyePosition = view.getCurrentEyePosition(); s = s.replaceAll("\\[cameraLat\\]", Double.toString(eyePosition.getLatitude().degrees)); s = s.replaceAll("\\[cameraLon\\]", Double.toString(eyePosition.getLongitude().degrees)); s = s.replaceAll("\\[cameraAlt\\]", Double.toString(eyePosition.getAltitude())); s = s.replaceAll("\\[horizFOV\\]", Double.toString(view.getFieldOfView().degrees)); Rectangle viewport = view.getViewport(); s = s.replaceAll("\\[horizPixels\\]", Integer.toString(viewport.width)); s = s.replaceAll("\\[vertPixels\\]", Integer.toString(viewport.height)); // TODO: Implement support for the remaining viewFormat parameters: [vertFov] [terrainEnabled]. if (queryString.length() > 0 && queryString.charAt(queryString.length() - 1) != '&') queryString.append('&'); queryString.append(s, s.startsWith("&") ? 1 : 0, s.length()); } } // Ignore the httpQuery if it's unspecified, or if an empty httpQuery is specified. String s = this.getHttpQuery(); if (s != null) s = s.trim(); if (!WWUtil.isEmpty(s)) { String clientName = Configuration.getStringValue(AVKey.NAME, Version.getVersionName()); String clientVersion = Configuration.getStringValue(AVKey.VERSION, Version.getVersionNumber()); //noinspection ConstantConditions s = s.replaceAll("\\[clientVersion\\]", clientVersion); s = s.replaceAll("\\[kmlVersion\\]", KMLConstants.KML_VERSION); s = s.replaceAll("\\[clientName\\]", clientName); s = s.replaceAll("\\[language\\]", Locale.getDefault().getLanguage()); if (queryString.length() > 0 && queryString.charAt(queryString.length() - 1) != '&') queryString.append('&'); queryString.append(s, s.startsWith("&") ? 1 : 0, s.length()); } if (queryString.length() > 0 && queryString.charAt(0) != '?') queryString.insert(0, '?'); return queryString.length() > 0 ? queryString.toString() : null; } /** * Returns a Sector that specifies the current visible bounds on the globe. If this link specifies a * viewBoundScale, this scales the visible bounds from its centroid by that factor, but limits the * bounds to [-90,90] latitude and [-180,180] longitude. This returns {@link * gov.nasa.worldwind.geom.Sector#EMPTY_SECTOR} if the globe is not visible. * * @param dc the DrawContext for which to compute the visible bounds. * * @return the current visible bounds on the specified DrawContext. */ protected Sector computeVisibleBounds(DrawContext dc) { if (dc.getVisibleSector() != null && this.getViewBoundScale() != null) { // If the DrawContext has a visible sector and a viewBoundScale is specified, compute the view bounding box // by scaling the DrawContext's visible sector from its centroid, based on the scale factor specified by // viewBoundScale. double centerLat = dc.getVisibleSector().getCentroid().getLatitude().degrees; double centerLon = dc.getVisibleSector().getCentroid().getLongitude().degrees; double latDelta = dc.getVisibleSector().getDeltaLatDegrees(); double lonDelta = dc.getVisibleSector().getDeltaLonDegrees(); // Limit the view bounding box to the standard LatLon range. This prevents a viewBoundScale greater than one // from creating a bounding box that extends beyond [-90,90] latitude or [-180,180] longitude. The factory // methods Angle.fromDegreesLatitude and Angle.fromDegreesLongitude automatically limit latitude and // longitude to these ranges. return new Sector( Angle.fromDegreesLatitude(centerLat - this.getViewBoundScale() * (latDelta / 2d)), Angle.fromDegreesLatitude(centerLat + this.getViewBoundScale() * (latDelta / 2d)), Angle.fromDegreesLongitude(centerLon - this.getViewBoundScale() * (lonDelta / 2d)), Angle.fromDegreesLongitude(centerLon + this.getViewBoundScale() * (lonDelta / 2d))); } else if (dc.getVisibleSector() != null) { // If the DrawContext has a visible sector but no viewBoundScale is specified, use the DrawContext's visible // sector as the view bounding box. return dc.getVisibleSector(); } else { // If the DrawContext does not have a visible sector, use the standard EMPTY_SECTOR as the view bounding // box. If the viewFormat contains bounding box parameters, we must substitute them with a valid value. In // this case we substitute them with 0. return Sector.EMPTY_SECTOR; } } @Override public void applyChange(KMLAbstractObject sourceValues) { if (!(sourceValues instanceof KMLLink)) { String message = Logging.getMessage("nullValue.SourceIsNull"); Logging.logger().warning(message); throw new IllegalArgumentException(message); } KMLLink link = (KMLLink) sourceValues; link.finalHref = null; link.hrefURL = null; link.refreshTask = null; link.updateTime.set(System.currentTimeMillis()); super.applyChange(sourceValues); this.onChange(new Message(KMLAbstractObject.MSG_LINK_CHANGED, this)); } /** A Runnable task that marks a KMLLink as updated when the task executes. */ class RefreshTask implements Runnable { /** Mark the link as updated. */ public void run() { // Mark the link as updated. KMLLink.this.setUpdateTime(System.currentTimeMillis()); // Trigger a repaint to cause the link to be refreshed. KMLLink.this.getRoot().requestRedraw(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy