src.gov.nasa.worldwindx.examples.util.BalloonController Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwind Show documentation
Show all versions of worldwind Show documentation
World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwindx.examples.util;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.event.*;
import gov.nasa.worldwind.exception.WWTimeoutException;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.layers.RenderableLayer;
import gov.nasa.worldwind.ogc.kml.*;
import gov.nasa.worldwind.ogc.kml.impl.*;
import gov.nasa.worldwind.pick.*;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.terrain.Terrain;
import gov.nasa.worldwind.util.*;
import gov.nasa.worldwindx.examples.kml.KMLViewController;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.Timer;
/**
* Controller to display a {@link Balloon} and handle balloon events. The controller does the following:
* - Display a balloon when an object is selected
- Handle URL selection events in balloons
- Resize
* BrowserBalloons
- Handle close, back, and forward events in BrowserBalloon
*
* Displaying a balloon for a selected object
*
* When a object is clicked, the controller looks for a Balloon attached to the object. The controller includes special
* logic for handling balloons attached to KML features.
*
* KML Features
*
* The KMLAbstractFeature is attached to the top PickedObject under AVKey.CONTEXT. The controller looks for the balloon
* in the KMLAbstractFeature under key AVKey.BALLOON.
*
* Other objects
*
* If the top object is an instance of AVList, the controller looks for a Balloon under AVKey.BALLOON.
*
* URL events
*
* The controller looks for a value under AVKey.URL attached to either the top PickedObject. If the URL refers to a KML
* or KMZ document, the document is loaded into a new layer. If the link includes a reference to a KML feature,
* controller will animate the view to that feature and/or open the feature balloon.
*
* If the link should open in a new window (determined by an AVKey.TARGET of "_blank"), the controller will launch the
* system web browser and navigate to the link. Otherwise it will allow the BrowserBalloon to navigate to the link.
*
* Consuming a SelectEvent in the BalloonController will prevent the balloon from taking action on that event. For
* example, a BrowserBalloon will navigate in place when a link is clicked, but it will not if the balloon controller
* consumes the left press and left click select events. This allows the balloon controller to override the default
* action for certain URLs.
*
* BrowserBalloon control events
*
* {@link gov.nasa.worldwind.render.AbstractBrowserBalloon} identifies its controls by attaching a value to the
* PickedObject's AVList under AVKey.ACTION. The controller reads this value and performs the appropriate action. The
* possible actions are AVKey.RESIZE, AVKey.BACK, AVKey.FORWARD, and AVKey.CLOSE.
*
* @author pabercrombie
* @version $Id: BalloonController.java 1531 2013-08-04 16:19:13Z pabercrombie $
*/
public class BalloonController extends MouseAdapter implements SelectListener
{
/* Default vertical offset, in pixels, between the balloon and the point that the leader shape points to. */
public static final int DEFAULT_BALLOON_OFFSET = 60;
public static final String FLY_TO = "flyto";
public static final String BALLOON = "balloon";
public static final String BALLOON_FLY_TO = "balloonFlyto";
protected WorldWindow wwd;
protected Object lastSelectedObject;
protected Balloon balloon;
/** Vertical offset, in pixels, between the balloon and the point that the leader shape points to. */
protected int balloonOffset = DEFAULT_BALLOON_OFFSET;
/**
* Timeout to use when requesting remote documents. If the document does not load within this many milliseconds the
* controller will stop trying and report an error.
*/
protected long retrievalTimeout = 30 * 1000; // 30 seconds
/** Interval between periodic checks for completion of asynchronous document retrieval (in milliseconds). */
protected long retrievalPollInterval = 1000; // 1 second
/**
* A resize controller is created when the mouse enters a resize control on the balloon. The controller is destroyed
* when the mouse exits the resize control.
*/
protected BalloonResizeController resizeController;
/**
* Create a new balloon controller.
*
* @param wwd WorldWindow to attach to.
*/
public BalloonController(WorldWindow wwd)
{
if (wwd == null)
{
String message = Logging.getMessage("nullValue.WorldWindow");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.wwd = wwd;
this.wwd.addSelectListener(this);
this.wwd.getInputHandler().addMouseListener(this);
this.wwd.getInputHandler().addMouseMotionListener(this);
}
/**
* Indicates the vertical distance, in pixels, between the balloon and the point that the leader points to.
*
* @return Vertical offset, in pixels.
*/
public int getBalloonOffset()
{
return this.balloonOffset;
}
/**
* Sets the vertical distance, in pixels, between the balloon and the point that the leader points to.
*
* @param balloonOffset Vertical offset, in pixels.
*/
public void setBalloonOffset(int balloonOffset)
{
this.balloonOffset = balloonOffset;
}
//********************************************************************//
//*********************** Event handling *****************************//
//********************************************************************//
/**
* Handle a mouse click. If the top picked object has a balloon attached to it the balloon will be made visible. A
* balloon may be attached to a KML feature, or to any picked object though {@link AVKey#BALLOON}.
*
* @param e Mouse event
*/
@Override
public void mouseClicked(MouseEvent e)
{
if (e == null || e.isConsumed())
return;
// Implementation note: handle the balloon with a mouse listener instead of a select listener so that the balloon
// can be turned off if the user clicks on the terrain.
try
{
if (this.isBalloonTrigger(e))
{
PickedObjectList pickedObjects = this.wwd.getObjectsAtCurrentPosition();
if (pickedObjects == null || pickedObjects.getTopPickedObject() == null)
{
this.hideBalloon();
return;
}
Object topObject = pickedObjects.getTopObject();
PickedObject topPickedObject = pickedObjects.getTopPickedObject();
boolean sameObjectSelected = this.lastSelectedObject == topObject || this.balloon == topObject;
boolean balloonVisible = this.balloon != null && this.balloon.isVisible();
// Do nothing if the same thing is selected and the balloon is already visible.
if (sameObjectSelected && balloonVisible)
return;
// Hide the active balloon if the selection has changed, or if terrain was selected.
if (this.balloon != null && !(topObject instanceof Balloon))
{
this.hideBalloon(); // Something else selected
}
Balloon balloon = this.getBalloon(topPickedObject);
// Don't change balloons that are already visible
if (balloon != null && !balloon.isVisible())
{
this.lastSelectedObject = topObject;
this.showBalloon(balloon, topObject, e.getPoint());
}
}
}
catch (Exception ex)
{
// Wrap the handler in a try/catch to keep exceptions from bubbling up
Logging.logger().warning(ex.getMessage() != null ? ex.getMessage() : ex.toString());
}
}
@Override
public void mouseMoved(MouseEvent e)
{
if (e == null || e.isConsumed())
return;
PickedObjectList list = this.wwd.getObjectsAtCurrentPosition();
PickedObject pickedObject = list != null ? list.getTopPickedObject() : null;
// Handle balloon resize events. Create a resize controller when the mouse enters the resize area.
// While the mouse is in the resize area, the resize controller will handle select events to resize the
// balloon. The controller will be destroyed when the mouse exists the resize area.
if (pickedObject != null && this.isResizeControl(pickedObject))
{
this.createResizeController((Balloon) pickedObject.getObject());
}
else if (this.resizeController != null && !this.resizeController.isResizing())
{
// Destroy the resize controller if the mouse is out of the resize area and the controller
// is not resizing the balloon. The mouse is allowed to move out of the resize area during the resize
// operation. If this event is a drag end, check the top object at the current position to determine if
// the cursor is still over the resize area.
this.destroyResizeController(null);
}
}
public void selected(SelectEvent event)
{
if (event == null || event.isConsumed()
|| (event.getMouseEvent() != null && event.getMouseEvent().isConsumed()))
{
return;
}
try
{
PickedObject pickedObject = event.getTopPickedObject();
if (pickedObject == null)
return;
Object topObject = event.getTopObject();
// Destroy the resize controller the event is a drag end and the mouse is out of the resize area, and the
// controller is not resizing the balloon. The mouse is allowed to move out of the resize area during the
// resize operation.
if (event.isDragEnd() && this.resizeController != null && !this.resizeController.isResizing())
{
PickedObject po;
PickedObjectList list = this.wwd.getObjectsAtCurrentPosition();
po = list != null ? list.getTopPickedObject() : null;
if (!this.isResizeControl(po))
{
this.destroyResizeController(event);
}
}
// Check to see if the event is a link activation or other balloon event
if (event.isLeftClick())
{
String url = this.getUrl(pickedObject);
if (url != null)
{
this.onLinkActivated(event, url);
}
else if (pickedObject.hasKey(AVKey.ACTION) && topObject instanceof AbstractBrowserBalloon)
{
this.onBalloonAction((AbstractBrowserBalloon) topObject, pickedObject.getStringValue(AVKey.ACTION));
}
}
else if (event.isLeftDoubleClick())
{
// Call onLinkActivated for left double click even though we don't want to follow links when these
// events occur. onLinkActivated determines if the URL is something that the controller should handle,
// and consume the event if so. onLinkActivated does not perform the associated link action unless the
// event is a left click. If we don't consume the event, the balloon may take action when a left press
// event occurs on a link that the balloon controller will handle (for example, a link to a KML file.)
// We avoid consuming left press events, since doing so prevents the WorldWindow from gaining focus.
String url = this.getUrl(pickedObject);
if (url != null)
{
this.onLinkActivated(event, url);
}
}
}
catch (Exception e)
{
// Wrap the handler in a try/catch to keep exceptions from bubbling up
Logging.logger().warning(e.getMessage() != null ? e.getMessage() : e.toString());
}
}
protected boolean isResizeControl(PickedObject po)
{
return po != null
&& AVKey.RESIZE.equals(po.getStringValue(AVKey.ACTION))
&& po.getObject() instanceof Balloon;
}
/**
* Get the URL attached to a PickedObject. This method looks for a URL attached to the PickedObject under {@link
* AVKey#URL}.
*
* @param pickedObject PickedObject to inspect. May not be null.
*
* @return The URL attached to the PickedObject, or null if there is no URL.
*/
protected String getUrl(PickedObject pickedObject)
{
return pickedObject.getStringValue(AVKey.URL);
}
/**
* Get the KML feature that is the context of a picked object. The context is associated with either the
* PickedObject or the user object under the key {@link AVKey#CONTEXT}.
*
* @param pickedObject PickedObject to inspect for context. May not be null.
*
* @return The KML feature associated with the picked object, or null if no KML feature is found.
*/
protected KMLAbstractFeature getContext(PickedObject pickedObject)
{
Object topObject = pickedObject.getObject();
Object context = pickedObject.getValue(AVKey.CONTEXT);
// If there was no context in the PickedObject, look for it in the top user object.
if (context == null && topObject instanceof AVList)
{
context = ((AVList) topObject).getValue(AVKey.CONTEXT);
}
if (context instanceof KMLAbstractFeature)
return (KMLAbstractFeature) context;
else
return null;
}
/**
* Called when a {@link gov.nasa.worldwind.render.AbstractBrowserBalloon} control is activated (Close, Back, or
* Forward).
*
* @param browserBalloon Balloon involved in action.
* @param action Identifier for the action that occurred.
*/
protected void onBalloonAction(AbstractBrowserBalloon browserBalloon, String action)
{
if (AVKey.CLOSE.equals(action))
{
// If the balloon closing is the balloon we manage, call hideBalloon to clean up state.
// Otherwise just make the balloon invisible.
if (browserBalloon == this.balloon)
this.hideBalloon();
else
browserBalloon.setVisible(false);
}
else if (AVKey.BACK.equals(action))
browserBalloon.goBack();
else if (AVKey.FORWARD.equals(action))
browserBalloon.goForward();
}
//********************************************************************//
//*********************** Resize events *****************************//
//********************************************************************//
/**
* Create a resize controller and attach it to the WorldWindow. Has no effect if there is already an active resize
* controller.
*
* @param balloon Balloon to resize.
*/
protected void createResizeController(Balloon balloon)
{
// If a resize controller is already active, don't start another one.
if (this.resizeController != null)
return;
this.resizeController = new BalloonResizeController(this.wwd, balloon);
}
/**
* Destroy the active resize controller.
*
* @param event Event that triggered the controller to be destroyed.
*/
protected void destroyResizeController(SelectEvent event)
{
if (this.resizeController != null)
{
try
{
// Pass the last event to the controller so that it can clean up internal state if it needs to.
if (event != null)
this.resizeController.selected(event);
this.resizeController.detach();
this.resizeController = null;
}
finally
{
// Reset the cursor to default. The resize controller may have changed it.
if (this.wwd instanceof Component)
{
((Component) this.wwd).setCursor(Cursor.getDefaultCursor());
}
}
}
}
//**********************************************************************//
//*********************** Hyperlink events ***************************//
//**********************************************************************//
/**
* Called when a URL in a balloon is activated. This method handles links to KML documents, features in KML
* documents, and links that target a new browser window.
*
* The possible cases are:
*
* KML/KMZ document - Load the document in a new layer.
Feature in KML/KMZ document - Load the
* document, navigate to the feature and/or open feature balloon.
Feature in currently open KML/KMZ
* document - Navigate to the feature and/or open feature balloon.
HTML document, target current
* window - No action, let the BrowserBalloon navigate to the URL.
HTML document, target new window
* - Launch the system web browser and navigate to the URL.
*
* If the URL matches one of the cases defined above, the SelectEvent will be marked as consumed. Marking the event
* as consumed prevents BrowserBalloon from handling the event. However, the controller will only take action on the
* event if the event is a link activation trigger.
*
* For example, if a left click event (a link activation event) occurs on a link to a KML document, the event will
* be marked as consumed and the document will be opened. If a left press event (not a link activation event) occurs
* with the same URL, the event will be consumed but the document will not be opened (if the press is followed by a
* click, the click will cause the document to be opened). Consuming the left press prevents the balloon from
* processing the event.
*
* @param event SelectEvent for the URL activation. If the event is a link activation trigger the controller will
* take action on the event (by opening a KML document, etc). If the event is not a link activation
* trigger, but the URL is a URL that the balloon controller would normally handle, the event is
* consumed to prevent the balloon itself from trying to handle the event, but no further action is
* taken.
* @param url URL that was activated.
*
* @see #isLinkActivationTrigger(gov.nasa.worldwind.event.SelectEvent)
*/
protected void onLinkActivated(SelectEvent event, String url)
{
PickedObject pickedObject = event.getTopPickedObject();
String type = pickedObject.getStringValue(AVKey.MIME_TYPE);
// Break URL into base and reference
String linkBase;
String linkRef;
int hashSign = url.indexOf("#");
if (hashSign != -1)
{
linkBase = url.substring(0, hashSign);
linkRef = url.substring(hashSign);
}
else
{
linkBase = url;
linkRef = null;
}
KMLRoot targetDoc; // The document to load and/or fly to
KMLRoot contextDoc = null; // The local KML document that initiated the link
KMLAbstractFeature kmlFeature;
boolean isKmlUrl = this.isKmlUrl(linkBase, type);
boolean foundLocalFeature = false;
// Look for a KML feature attached to the picked object. If present, the link will be interpreted relative
// to this feature.
kmlFeature = this.getContext(pickedObject);
if (kmlFeature != null)
contextDoc = kmlFeature.getRoot();
// If this link is to a KML or KMZ document we will load the document into a new layer.
if (isKmlUrl)
{
targetDoc = this.findOpenKmlDocument(linkBase);
if (targetDoc == null)
{
// Asynchronously request the document if the event is a link activation trigger.
if (this.isLinkActivationTrigger(event))
this.requestDocument(linkBase, contextDoc, linkRef);
// We are opening a document, consume the event to prevent balloon from trying to load the document.
event.consume();
return;
}
}
else
{
// URL does not refer to a remote KML document, assume that it refers to a feature in the current doc
targetDoc = contextDoc;
}
// If the link also has a feature reference, we will move to the feature
if (linkRef != null)
{
if (this.onFeatureLinkActivated(targetDoc, linkRef, event))
{
foundLocalFeature = true;
event.consume(); // Consume event if the target feature was found
}
}
// If the link is not to a KML file or feature, and the link targets a new browser window, launch the system web
// browser. BrowserBalloon ignores link events that target new windows, so we need to handle them here.
if (!isKmlUrl && !foundLocalFeature)
{
String target = pickedObject.getStringValue(AVKey.TARGET);
if ("_blank".equalsIgnoreCase(target))
{
// Invoke the system browser to open the link if the event is link activation trigger.
if (this.isLinkActivationTrigger(event))
this.openInNewBrowser(event, url);
event.consume();
}
}
}
/**
* Determines if a SelectEvent is an event that activates a hyperlink.
*
* @param event Event to test. May not be null.
*
* @return {@code true} if the event actives hyperlinks. This implementation returns {@code true} for left click
* events.
*/
protected boolean isLinkActivationTrigger(SelectEvent event)
{
return event.isLeftClick();
}
/**
* Open a URL in a new web browser. Launch the system web browser and navigate to the URL.
*
* @param event SelectEvent that triggered navigation. The event is consumed if URL can be parsed.
* @param url URL to open.
*/
protected void openInNewBrowser(SelectEvent event, String url)
{
try
{
BrowserOpener.browse(new URL(url));
event.consume();
}
catch (Exception e)
{
String message = Logging.getMessage("generic.ExceptionAttemptingToInvokeWebBrower", url);
Logging.logger().warning(message);
}
}
/**
* Called when a link to a KML feature is activated.
*
* @param doc Document to search for the feature.
* @param linkFragment Reference to the feature. The fragment may contain a display directive. For example
* "#myPlacemark", or "#myPlacemark;balloon".
* @param event The select event that activated the link. This event will be consumed if a KML feature is
* found that matches the link fragment. However, the controller only moves to the feature or
* opens a balloon if the event is a link activation event, or null. Other events are consumed
* to prevent the balloon from handling events for a link that the controller wants handle. This
* parameter may be null.
*
* @return True if a feature matching the reference was found and some action was taken.
*/
protected boolean onFeatureLinkActivated(KMLRoot doc, String linkFragment, SelectEvent event)
{
// Split the reference into the feature id and the display directive (flyto, balloon, etc)
String[] parts = linkFragment.split(";");
String featureId = parts[0];
String directive = parts.length > 1 ? parts[1] : FLY_TO;
if (!WWUtil.isEmpty(featureId) && doc != null)
{
Object o = doc.resolveReference(featureId);
if (o instanceof KMLAbstractFeature)
{
// Perform the link action if the event is a link activation event.
if (event == null || this.isLinkActivationTrigger(event))
this.doFeatureLinkActivated((KMLAbstractFeature) o, directive);
return true;
}
}
return false;
}
/**
* Handle activation of a KML feature link. Depending on the display directive, this method will either move the
* view to the feature, open the balloon for the feature, or both. See the KML specification for details on links to
* features in the KML description balloon.
*
* @param feature Feature to navigate to.
* @param directive Display directive, one of {@link #FLY_TO}, {@link #BALLOON}, or {@link #BALLOON_FLY_TO}.
*/
protected void doFeatureLinkActivated(KMLAbstractFeature feature, String directive)
{
if (FLY_TO.equals(directive) || BALLOON_FLY_TO.equals(directive))
{
this.moveToFeature(feature);
}
if (BALLOON.equals(directive) || BALLOON_FLY_TO.equals(directive))
{
this.showBalloon(feature);
}
}
/**
* Does a URL refer to a KML or KMZ document?
*
* @param url URL to test.
* @param contentType Mime type of the URL content. May be null.
*
* @return Return true if the URL refers to a file with a ".kml" or ".kmz" extension, or if the {@code contentType}
* is the KML or KMZ mime type.
*/
protected boolean isKmlUrl(String url, String contentType)
{
if (WWUtil.isEmpty(url))
return false;
String suffix = WWIO.getSuffix(url);
return "kml".equalsIgnoreCase(suffix)
|| "kmz".equalsIgnoreCase(suffix)
|| KMLConstants.KML_MIME_TYPE.equals(contentType)
|| KMLConstants.KMZ_MIME_TYPE.equals(contentType);
}
/**
* Move the view to look at a KML feature. The view will be adjusted to look at the bounding sector that contains
* all of the feature's points.
*
* @param feature Feature to look at.
*/
protected void moveToFeature(KMLAbstractFeature feature)
{
KMLViewController viewController = KMLViewController.create(this.wwd);
viewController.goTo(feature);
}
//**********************************************************************//
//********************** Show/Hide Balloon ***************************//
//**********************************************************************//
/**
* Show a balloon for a KML feature. The balloon will be positioned over the feature on the globe. If the feature
* does not have a balloon, a balloon may be created. {@link #canShowBalloon(gov.nasa.worldwind.ogc.kml.KMLAbstractFeature)
* canShowBalloon} determines if a balloon will be created.
*
* @param feature KML feature for which to show a balloon.
*/
public void showBalloon(KMLAbstractFeature feature)
{
Balloon balloon = feature.getBalloon();
// Create a new balloon if the feature does not have one
if (balloon == null && this.canShowBalloon(feature))
balloon = this.createBalloon(feature);
// Don't change balloons that are already visible
if (balloon != null && !balloon.isVisible())
{
this.lastSelectedObject = feature;
Position pos = this.getBalloonPosition(feature);
if (pos != null)
{
this.hideBalloon(); // Hide previously displayed balloon, if any
this.showBalloon(balloon, pos);
}
else
{
// The feature may be attached to the screen, not the globe
Point point = this.getBalloonPoint(feature);
if (point != null)
{
this.hideBalloon(); // Hide previously displayed balloon, if any
this.showBalloon(balloon, null, point);
}
// If the feature is not attached to a particular point, just put it in the middle of the viewport
else
{
Rectangle viewport = this.wwd.getView().getViewport();
Point center = new Point((int) viewport.getCenterX(), (int) viewport.getCenterY());
this.hideBalloon();
this.showBalloon(balloon, null, center);
}
}
}
}
/**
* Determines whether or not a balloon must be created for a KML feature. A balloon is created for any feature with
* a balloon style or a non-empty description. No balloon is created for a feature with no balloon style and no
* description.
*
* @param feature KML feature to test.
*
* @return {@code true} if a balloon must be created for the feature. Otherwise {@code false}.
*/
public boolean canShowBalloon(KMLAbstractFeature feature)
{
KMLBalloonStyle style = (KMLBalloonStyle) feature.getSubStyle(new KMLBalloonStyle(null), KMLConstants.NORMAL);
boolean isBalloonHidden = "hide".equals(style.getDisplayMode());
// Determine if the balloon style actually has fields.
boolean hasBalloonStyle = style.hasStyleFields() && !style.hasField(AVKey.UNRESOLVED);
// Do not create a balloon if there is no balloon style and the feature has no description.
return (hasBalloonStyle || !WWUtil.isEmpty(feature.getDescription()) || feature.getExtendedData() != null)
&& !isBalloonHidden;
}
/**
* Inspect a mouse event to see if it should make a balloon visible.
*
* @param e Event to inspect.
*
* @return {@code true} if the event is a balloon trigger. This implementation returns {@code true} if the event is
* a left click.
*/
protected boolean isBalloonTrigger(MouseEvent e)
{
// Handle only left click
return (e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() % 2 == 1);
}
/**
* Get the balloon attached to a PickedObject. If the PickedObject represents a KML feature, then the balloon will
* be retrieved from the feature. Otherwise, the balloon will be retrieved from the user object's field
* AVKey.BALLOON.
*
* If a KML feature is picked, and the feature does not have a balloon, a new balloon may be created and attached to
* the feature. {@link #canShowBalloon(gov.nasa.worldwind.ogc.kml.KMLAbstractFeature) canShowBalloon} determines if
* a balloon will be created for the feature.
*
* @param pickedObject PickedObject to inspect. May not be null.
*
* @return The balloon attached to the picked object, or null if there is no balloon. Returns null if {@code
* pickedObject} is null.
*/
protected Balloon getBalloon(PickedObject pickedObject)
{
Object topObject = pickedObject.getObject();
Object balloonObj = null;
// Look for a KMLAbstractFeature context. If the top picked object is part of a KML feature, the
// feature will determine the balloon.
if (pickedObject.hasKey(AVKey.CONTEXT))
{
Object contextObj = pickedObject.getValue(AVKey.CONTEXT);
if (contextObj instanceof KMLAbstractFeature)
{
KMLAbstractFeature feature = (KMLAbstractFeature) contextObj;
balloonObj = feature.getBalloon();
// Create a new balloon if the feature does not have one
if (balloonObj == null && this.canShowBalloon(feature))
balloonObj = this.createBalloon(feature);
}
}
// If we didn't find a balloon on the KML feature, look for a balloon in the AVList
if (balloonObj == null && topObject instanceof AVList)
{
AVList avList = (AVList) topObject;
balloonObj = avList.getValue(AVKey.BALLOON);
}
if (balloonObj instanceof Balloon)
return (Balloon) balloonObj;
else
return null;
}
/**
* Create a balloon for a KML feature and attach the balloon to the feature. The type of balloon created depends on
* the type of feature and the result of {@link #isUseBrowserBalloon()}. If the feature is attached to a point on
* the globe, this method creates a {@link GlobeBalloon}. If the feature is attached to the screen, a {@link
* ScreenBalloon} is created. If isUseBrowserBalloon() returns {@code true}, the balloon will be a descendant of
* {@link AbstractBrowserBalloon}. Otherwise it will be a descendant of {@link AbstractAnnotationBalloon}.
*
* @param feature Feature to create balloon for.
*
* @return New balloon. May return null if the feature should not have a balloon.
*/
protected Balloon createBalloon(KMLAbstractFeature feature)
{
KMLBalloonStyle balloonStyle = (KMLBalloonStyle) feature.getSubStyle(new KMLBalloonStyle(null),
KMLConstants.NORMAL);
String text = balloonStyle.getText();
if (text == null)
text = "";
// Create the balloon based on the features attachment mode and the browser balloon settings. Wrap the balloon
// in a KMLBalloonImpl to handle balloon style resolution.
KMLAbstractBalloon kmlBalloon;
if (AVKey.GLOBE.equals(this.getAttachmentMode(feature)))
{
GlobeBalloon balloon;
if (this.isUseBrowserBalloon())
balloon = new GlobeBrowserBalloon(text, Position.ZERO); // 0 is dummy position
else
balloon = new GlobeAnnotationBalloon(text, Position.ZERO); // 0 is dummy position
kmlBalloon = new KMLGlobeBalloonImpl(balloon, feature);
}
else
{
ScreenBalloon balloon;
if (this.isUseBrowserBalloon())
balloon = new ScreenBrowserBalloon(text, new Point(0, 0)); // 0,0 is dummy position
else
balloon = new ScreenAnnotationBalloon(text, new Point(0, 0)); // 0,0 is dummy position
kmlBalloon = new KMLScreenBalloonImpl(balloon, feature);
}
kmlBalloon.setVisible(false);
kmlBalloon.setAlwaysOnTop(true);
// Attach the balloon to the feature
feature.setBalloon(kmlBalloon);
this.configureBalloon(kmlBalloon, feature);
return kmlBalloon;
}
/**
* Configure a new balloon for a KML feature.
*
* @param balloon Balloon to configure.
* @param feature Feature that owns the Balloon.
*/
protected void configureBalloon(Balloon balloon, KMLAbstractFeature feature)
{
// Configure the balloon for a container to not have a leader. These balloons will display in the middle of the
// viewport.
if (feature instanceof KMLAbstractContainer)
{
BalloonAttributes attrs = new BasicBalloonAttributes();
// Size the balloon to match the size of the content.
Size size = new Size(Size.NATIVE_DIMENSION, 0.0, null, Size.NATIVE_DIMENSION, 0.0, null);
// Do not allow the balloon to be auto-sized larger than 80% of the viewport. The user may resize the balloon
// larger than this size.
Size maxSize = new Size(Size.EXPLICIT_DIMENSION, 0.8, AVKey.FRACTION,
Size.EXPLICIT_DIMENSION, 0.8, AVKey.FRACTION);
attrs.setSize(size);
attrs.setMaximumSize(maxSize);
attrs.setOffset(new Offset(0.5, 0.5, AVKey.FRACTION, AVKey.FRACTION));
attrs.setLeaderShape(AVKey.SHAPE_NONE);
balloon.setAttributes(attrs);
}
else
{
BalloonAttributes attrs = new BasicBalloonAttributes();
// Size the balloon to match the size of the content.
Size size = new Size(Size.NATIVE_DIMENSION, 0.0, null, Size.NATIVE_DIMENSION, 0.0, null);
// Do not allow the balloon to be auto-sized larger than 50% of the viewport width, and 40% of the height.
// The user may resize the balloon larger than this size.
Size maxSize = new Size(Size.EXPLICIT_DIMENSION, 0.5, AVKey.FRACTION,
Size.EXPLICIT_DIMENSION, 0.4, AVKey.FRACTION);
attrs.setSize(size);
attrs.setMaximumSize(maxSize);
balloon.setAttributes(attrs);
}
}
/**
* Get the attachment mode of a KML feature: {@link AVKey#GLOBE} or {@link AVKey#SCREEN}. Some features, such as a
* PointPlacemark, are attached to a point on the globe. Others, such as a ScreenImage, are attached to the screen.
*
* @param feature KML feature to test.
*
* @return {@link AVKey#GLOBE} if the feature is attached to a geographic location. Otherwise {@link AVKey#SCREEN}.
* Container features (Document and Folder) are considered screen features.
*/
protected String getAttachmentMode(KMLAbstractFeature feature)
{
if (feature instanceof KMLPlacemark || feature instanceof KMLGroundOverlay)
return AVKey.GLOBE;
else
return AVKey.SCREEN;
}
/**
* Indicates if the controller will create Balloons of type {@link AbstractBrowserBalloon}. BrowserBalloons are used
* on platforms that support them (currently Windows and Mac). {@link AbstractAnnotationBalloon} is used on other
* platforms.
*
* @return {@code true} if the controller will create BrowserBalloons.
*/
protected boolean isUseBrowserBalloon()
{
return Configuration.isWindowsOS() || Configuration.isMacOS();
}
/**
* Show a balloon at a screen point.
*
* @param balloon Balloon to make visible.
* @param balloonObject The picked object that owns the balloon. May be {@code null}.
* @param point Point where mouse was clicked.
*/
protected void showBalloon(Balloon balloon, Object balloonObject, Point point)
{
// If the balloon is attached to the screen rather than the globe, move it to the
// current point. Otherwise move it to the position under the current point.
if (balloon instanceof ScreenBalloon)
((ScreenBalloon) balloon).setScreenLocation(point);
else if (balloon instanceof GlobeBalloon)
{
Position position = this.getBalloonPosition(balloonObject, point);
if (position != null)
{
GlobeBalloon globeBalloon = (GlobeBalloon) balloon;
globeBalloon.setPosition(position);
globeBalloon.setAltitudeMode(this.getBalloonAltitudeMode(balloonObject));
}
}
if (this.mustAdjustPosition(balloon))
this.adjustPosition(balloon, point);
this.balloon = balloon;
this.balloon.setVisible(true);
}
/**
* Show a balloon at a globe position.
*
* @param balloon Balloon to make visible.
* @param position Position on the globe to locate the balloon. If the balloon is attached to the screen, it will be
* position at the screen point currently over this position.
*/
protected void showBalloon(Balloon balloon, Position position)
{
Vec4 screenVec4 = this.wwd.getView().project(
this.wwd.getModel().getGlobe().computePointFromPosition(position));
Point screenPoint = new Point((int) screenVec4.x,
(int) (this.wwd.getView().getViewport().height - screenVec4.y));
// If the balloon is attached to the screen rather than the globe, move it to the
// current point. Otherwise move it to the position under the current point.
if (balloon instanceof ScreenBalloon)
{
((ScreenBalloon) balloon).setScreenLocation(screenPoint);
}
else
{
((GlobeBalloon) balloon).setPosition(position);
}
if (this.mustAdjustPosition(balloon))
this.adjustPosition(balloon, screenPoint);
this.balloon = balloon;
this.balloon.setVisible(true);
}
/**
* Determines if a balloon position must be adjusted to make the balloon visible in the viewport.
*
* @param balloon Balloon to inspect.
*
* @return {@code true} if the balloon position must be adjusted to make the balloon visible.
*/
protected boolean mustAdjustPosition(Balloon balloon)
{
// Look at the balloon leader shape. If there is no leader shape, assume that the balloon itself is positioned
// over the point of interest, and cannot be moved. Otherwise, assume that the balloon must be adjusted.
BalloonAttributes attrs = balloon.getAttributes();
return !(AVKey.SHAPE_NONE.equals(attrs.getLeaderShape()));
}
/**
* Adjust the position of a balloon so that the entire balloon is visible on screen.
*
* @param balloon Balloon to adjust the position of.
* @param screenPoint Screen point to which the balloon leader points.
*/
protected void adjustPosition(Balloon balloon, Point screenPoint)
{
// Create an offset that will ensure that the balloon is visible. This method assumes that the balloon
// width is less than half of the viewport width, and that the balloon height is less half of the viewport
// height, the default maximum size applied to balloons created by the controller.
Rectangle viewport = this.wwd.getView().getViewport();
double x, y;
String xUnits, yUnits;
// If the balloon point is in the right 25% of the viewport, place the balloon to the left.
xUnits = AVKey.FRACTION;
if (screenPoint.x > viewport.width * 0.75)
{
x = 1.0;
}
// If the point is in the left 25% of the viewport, place the balloon to the right.
else if (screenPoint.x < viewport.width * 0.25)
{
x = 0;
}
// Otherwise, center the balloon on the point.
else
{
x = 0.5;
}
int vertOffset = this.getBalloonOffset();
y = -vertOffset;
// If the point is in the top half of the viewport, place the balloon below the point.
if (screenPoint.y < viewport.height * 0.5)
{
yUnits = AVKey.INSET_PIXELS;
}
// Otherwise, place the balloon above the point.
else
{
yUnits = AVKey.PIXELS;
}
Offset offset = new Offset(x, y, xUnits, yUnits);
BalloonAttributes attributes = balloon.getAttributes();
if (attributes == null)
{
attributes = new BasicBalloonAttributes();
balloon.setAttributes(attributes);
}
attributes.setOffset(offset);
BalloonAttributes highlightAttributes = balloon.getHighlightAttributes();
if (highlightAttributes != null)
highlightAttributes.setOffset(offset);
}
/** Hide the active balloon. Does nothing if there is no active balloon. */
protected void hideBalloon()
{
if (this.balloon != null)
{
this.balloon.setVisible(false);
this.balloon = null;
}
this.lastSelectedObject = null;
}
//**********************************************************************//
//*********** Methods to determine where to put the balloon **********//
//**********************************************************************//
/**
* Get the position of the balloon for a KML feature attached to the globe. This method applies to KML features that
* area attached to the globe, rather than to the screen (for example, this method applies to GroundOverlay, but not
* to ScreenOverlay). This method determines the type of feature, and calls a more specific method to handle
* features of that type.
*
* @param feature Feature to find balloon position for.
*
* @return Position at which to place the Placemark balloon.
*
* @see #getBalloonPositionForPlacemark(gov.nasa.worldwind.ogc.kml.KMLPlacemark)
* @see #getBalloonPositionForGroundOverlay(gov.nasa.worldwind.ogc.kml.KMLGroundOverlay)
* @see #getBalloonPoint(gov.nasa.worldwind.ogc.kml.KMLAbstractFeature)
*/
protected Position getBalloonPosition(KMLAbstractFeature feature)
{
if (feature instanceof KMLPlacemark)
{
return this.getBalloonPositionForPlacemark((KMLPlacemark) feature);
}
else if (feature instanceof KMLGroundOverlay)
{
return this.getBalloonPositionForGroundOverlay(((KMLGroundOverlay) feature));
}
return null;
}
/**
* Get the position of the balloon for a picked object with an attached balloon. If the top object is an instance of
* {@link Locatable}, this method returns the position of the Locatable. If the object is an instance of {@link
* AbstractShape}, the method performs an intersection calculation between a ray through the pick point and the
* shape. If neither of the previous conditions are true, or if the object is {@code null}, this method returns the
* intersection position of a ray through the pick point and the globe.
*
* @param topObject Object that was picked. May be {@code null}.
* @param pickPoint The point at which the mouse event occurred.
*
* @return Position at which to place the balloon, or {@code null} if a position cannot be determined.
*/
protected Position getBalloonPosition(Object topObject, Point pickPoint)
{
Position position = null;
if (topObject instanceof Locatable)
{
position = ((Locatable) topObject).getPosition();
}
else if (topObject instanceof AbstractShape)
{
position = this.computeIntersection((AbstractShape) topObject, pickPoint);
}
// Fall back to a terrain intersection if we still don't have a position.
if (position == null)
{
Line ray = this.wwd.getView().computeRayFromScreenPoint(pickPoint.x, pickPoint.y);
Intersection[] inter = this.wwd.getSceneController().getDrawContext().getSurfaceGeometry().intersect(ray);
if (inter != null && inter.length > 0)
{
position = this.wwd.getModel().getGlobe().computePositionFromPoint(inter[0].getIntersectionPoint());
}
// We still don't have a position, fall back to intersection with the ellipsoid.
if (position == null)
{
position = this.wwd.getView().computePositionFromScreenPoint(pickPoint.x, pickPoint.y);
}
}
return position;
}
/**
* Get the appropriate altitude mode for a GlobeBalloon, depending on the object that has been selected. If the
* balloon object is an instance of {@link PointPlacemark}, this implementation returns the altitude mode of the
* placemark. Otherwise it returns {@link WorldWind#ABSOLUTE}.
*
* @param balloonObject The object that the balloon is attached to.
*
* @return The altitude mode that should be applied to the balloon, one of {@link WorldWind#ABSOLUTE}, {@link
* WorldWind#CLAMP_TO_GROUND}, or {@link WorldWind#RELATIVE_TO_GROUND}.
*/
protected int getBalloonAltitudeMode(Object balloonObject)
{
// Balloons are often attached to PointPlacemarks, so handle this case specially. The balloon altitude mode
// needs to match the placemark altitude mode. Shapes do not have this problem because an intersection calculation
// can place the balloon.
if (balloonObject instanceof PointPlacemark)
{
return ((PointPlacemark) balloonObject).getAltitudeMode();
}
return WorldWind.ABSOLUTE; // Default to absolute
}
/**
* Compute the intersection of a line through a screen point and a shape.
*
* @param shape Shape with which to compute intersection.
* @param screenPoint Compute the intersection of a line through this screen point and the shape.
*
* @return The intersection position, or {@code null} if there is no intersection, or if the computation is
* interrupted.
*/
protected Position computeIntersection(AbstractShape shape, Point screenPoint)
{
try
{
// Compute the intersection using whatever terrain is available. This calculation does not need to be very
// precise, it just needs to place the balloon close to the shape.
Terrain terrain = this.wwd.getSceneController().getDrawContext().getTerrain();
// Compute a line through the pick point.
Line line = this.wwd.getView().computeRayFromScreenPoint(screenPoint.x, screenPoint.y);
// Find the intersection of the line and the shape.
List intersections = shape.intersect(line, terrain);
if (intersections != null && !intersections.isEmpty())
return intersections.get(0).getIntersectionPosition();
}
catch (InterruptedException ignored)
{
// Do nothing
}
return null;
}
/**
* Get the position of the balloon for a KML placemark. For a point placemark, this method returns the placemark
* point. For all other placemarks, this method returns the centroid of the sector that bounds all of the points in
* the placemark. Note that the centroid of the sector may not actually fall on the visible area of the shape.
*
* @param placemark Placemark for which to find a balloon position.
*
* @return Position for the balloon, or null if a position cannot be determined.
*
* @see #getBalloonPosition(gov.nasa.worldwind.ogc.kml.KMLAbstractFeature)
*/
protected Position getBalloonPositionForPlacemark(KMLPlacemark placemark)
{
List positions = new ArrayList();
KMLAbstractGeometry geometry = placemark.getGeometry();
KMLUtil.getPositions(this.wwd.getModel().getGlobe(), geometry, positions);
return this.getBalloonPosition(positions);
}
/**
* Get the position of the balloon for a KML GroundOverlay. This method returns the centroid of the sector that
* bounds all of the points in the overlay.
*
* @param overlay Ground overlay for which to find a balloon position.
*
* @return Position for the balloon, or null if a position cannot be determined.
*
* @see #getBalloonPosition(gov.nasa.worldwind.ogc.kml.KMLAbstractFeature)
*/
protected Position getBalloonPositionForGroundOverlay(KMLGroundOverlay overlay)
{
Position.PositionList positionsList = overlay.getPositions();
return this.getBalloonPosition(positionsList.list);
}
/**
* Get the position of the balloon for a list of positions that bound a feature. This method returns a position at
* the centroid of the sector that bounds all of the points in the list, and at the maximum altitude of the points
* in the list.
*
* @param positions List of positions to find a balloon position.
*
* @return Position for the balloon, or null if a position cannot be determined.
*/
protected Position getBalloonPosition(List extends Position> positions)
{
if (positions.size() == 1) // Only one point, just return the point
{
return positions.get(0);
}
else if (positions.size() > 1)// Many points, find center point of bounding sector
{
Sector sector = Sector.boundingSector(positions);
return new Position(sector.getCentroid(), this.findMaxAltitude(positions));
}
return null;
}
/**
* Get the screen point for a balloon for a KML feature attached to the screen. This method applies only to KML
* features that area attached to the screen, rather than to the globe (for example, ScreenOverlay, but not
* GroundOverlay). This method determines the type of feature, and then calls a more specific method to handle
* features of that type.
*
* @param feature Feature for which to find a balloon point.
*
* @return Point for the balloon, or null if a point cannot be determined.
*
* @see #getBalloonPointForScreenOverlay(gov.nasa.worldwind.ogc.kml.KMLScreenOverlay)
* @see #getBalloonPosition(gov.nasa.worldwind.ogc.kml.KMLAbstractFeature)
*/
protected Point getBalloonPoint(KMLAbstractFeature feature)
{
if (feature instanceof KMLScreenOverlay)
{
return this.getBalloonPointForScreenOverlay((KMLScreenOverlay) feature);
}
return null;
}
/**
* Get the screen point for a balloon for a ScreenOverlay.
*
* @param overlay ScreenOverlay for which to find a balloon position.
*
* @return Point for the balloon, or null if a point cannot be determined.
*
* @see #getBalloonPoint(gov.nasa.worldwind.ogc.kml.KMLAbstractFeature)
*/
protected Point getBalloonPointForScreenOverlay(KMLScreenOverlay overlay)
{
KMLVec2 xy = overlay.getScreenXY();
Offset offset = new Offset(xy.getX(), xy.getY(), KMLUtil.kmlUnitsToWWUnits(xy.getXunits()),
KMLUtil.kmlUnitsToWWUnits(xy.getYunits()));
Rectangle viewport = this.wwd.getView().getViewport();
Point2D point2D = offset.computeOffset(viewport.width, viewport.height, 1d, 1d);
int y = (int) point2D.getY();
return new Point((int) point2D.getX(), viewport.height - y);
}
/**
* Get the maximum altitude in a list of positions.
*
* @param positions List of positions to search for max altitude.
*
* @return The maximum elevation in the list of positions. Returns {@code -Double.MAX_VALUE} if {@code positions} is
* empty.
*/
protected double findMaxAltitude(List extends Position> positions)
{
double maxAltitude = -Double.MAX_VALUE;
for (Position p : positions)
{
double altitude = p.getAltitude();
if (altitude > maxAltitude)
maxAltitude = altitude;
}
return maxAltitude;
}
//**********************************************************************//
//****************** Remote document retrieval ***********************//
//**********************************************************************//
/**
* Search for a KML document that has already been opened. This method looks in the session cache for a parsed
* KMLRoot.
*
* @param url URL of the KML document.
*
* @return KMLRoot for an already-parsed document, or null if the document was not found in the cache.
*/
protected KMLRoot findOpenKmlDocument(String url)
{
Object o = WorldWind.getSessionCache().get(url);
if (o instanceof KMLRoot)
return (KMLRoot) o;
else
return null;
}
/**
* Asynchronously load a KML document. When the document is available, {@link #onDocumentLoaded(String,
* gov.nasa.worldwind.ogc.kml.KMLRoot, String) onDocumentLoaded} will be called on the Event Dispatch Thread (EDT).
* If the document fails to load, {@link #onDocumentFailed(String, Exception) onDocumentFailed} will be called.
* Failure will be reported if the document does not load within {@link #retrievalTimeout} milliseconds.
*
* @param url URL of KML doc to open.
* @param context Context of the URL, used to resolve local references.
* @param featureRef A reference to a feature in the remote file to animate the globe to once the file is
* available.
*
* @see #onDocumentLoaded(String, gov.nasa.worldwind.ogc.kml.KMLRoot, String)
* @see #onDocumentFailed(String, Exception)
*/
protected void requestDocument(String url, KMLRoot context, String featureRef)
{
Timer docLoader = new Timer("BalloonController document retrieval");
// Schedule a task that will request the document periodically until the document becomes available or the
// request timeout is reached.
docLoader.scheduleAtFixedRate(new DocumentRetrievalTask(url, context, featureRef, this.retrievalTimeout),
0, this.retrievalPollInterval);
}
/**
* Called when a KML document has been loaded. This implementation creates a new layer and adds the new document to
* the layer.
*
* @param url URL of the document that has been loaded.
* @param document Parsed document.
* @param featureRef Reference to a feature that must be activated (fly to or open balloon).
*/
protected void onDocumentLoaded(String url, KMLRoot document, String featureRef)
{
// Use the URL as the document's DISPLAY_NAME. This field is used by addDocumentLayer to determine the layer's
// name.
document.setField(AVKey.DISPLAY_NAME, url);
this.addDocumentLayer(document);
if (featureRef != null)
this.onFeatureLinkActivated(document, featureRef, null);
}
/**
* Called when a KML file fails to load due to a network timeout or parsing error. This implementation simply logs a
* warning.
*
* @param url URL of the document that failed to load.
* @param e Exception that caused the failure.
*/
protected void onDocumentFailed(String url, Exception e)
{
String message = Logging.getMessage("generic.ExceptionWhileReading", url + ": " + e.getMessage());
Logging.logger().warning(message);
}
/**
* Adds the specified document
to this controller's WorldWindow
as a new
* Layer
.
*
* This expects the kmlRoot
's AVKey.DISPLAY_NAME
field to contain a display name suitable
* for use as a layer name.
*
* @param document the KML document to add a Layer
for.
*/
protected void addDocumentLayer(KMLRoot document)
{
KMLController controller = new KMLController(document);
// Load the document into a new layer.
RenderableLayer kmlLayer = new RenderableLayer();
kmlLayer.setName((String) document.getField(AVKey.DISPLAY_NAME));
kmlLayer.addRenderable(controller);
this.wwd.getModel().getLayers().add(kmlLayer);
}
/**
* A TimerTask that will request a resource from the {@link gov.nasa.worldwind.cache.FileStore} until it becomes
* available, or until a timeout is exceeded. When the task finishes it will trigger a callback on the Event
* Dispatch Thread (EDT) to either {@link BalloonController#onDocumentLoaded(String,
* gov.nasa.worldwind.ogc.kml.KMLRoot, String) onDocumentLoaded} or {@link BalloonController#onDocumentFailed(String,
* Exception) onDocumentFailed}.
*
* This task is designed to be repeated periodically. The task will cancel itself when the document becomes
* available, or the timeout is exceeded.
*/
protected class DocumentRetrievalTask extends TimerTask
{
/** URL of the KML document to load. */
protected String docUrl;
/** The document that contained the link this document. */
protected KMLRoot context;
/**
* Reference to a feature in the remote document, with an action (for example, "myFeature;flyto"). The action
* will be carried out when the document becomes available.
*/
protected String featureRef;
/**
* Task timeout. If the document has not been loaded after this many milliseconds, the task will cancel itself
* and report an error.
*/
protected long timeout;
/** Time that the task started, used to evaluate the timeout. */
protected long start;
/**
* Create a new retrieval task.
*
* @param url URL of document to retrieve.
* @param context Context of the link to the document. May be null.
* @param featureRef Reference to a feature in the remote document, with an action to perform on the feature
* (for example, "myFeature;flyto"). The action will be carried out when the document becomes
* available.
* @param timeout Timeout for this task in milliseconds. The task will fail if the document has not been
* downloaded in this many milliseconds.
*/
public DocumentRetrievalTask(String url, KMLRoot context, String featureRef, long timeout)
{
this.docUrl = url;
this.context = context;
this.featureRef = featureRef;
this.timeout = timeout;
}
/**
* Request the document from the {@link gov.nasa.worldwind.cache.FileStore}. If the document is available, parse
* it and schedule a callback on the EDT to {@link BalloonController#onDocumentLoaded(String,
* gov.nasa.worldwind.ogc.kml.KMLRoot, String)}. If an exception occurs, or the timeout is exceeded, schedule a
* callback on the EDT to {@link BalloonController#onDocumentFailed(String, Exception)}
*/
public void run()
{
KMLRoot root = null;
try
{
// If this is the first execution, capture the start time so that we can evaluate the timeout later.
if (this.start == 0)
this.start = System.currentTimeMillis();
// Check for timeout before doing any work
if (System.currentTimeMillis() > this.start + this.timeout)
throw new WWTimeoutException(Logging.getMessage("generic.CannotOpenFile", this.docUrl));
// If we have a context document, let that doc resolve the reference. Otherwise, request it from the
// file store.
Object docSource;
if (this.context != null)
docSource = this.context.resolveReference(this.docUrl);
else
docSource = WorldWind.getDataFileStore().requestFile(this.docUrl);
if (docSource instanceof KMLRoot)
{
root = (KMLRoot) docSource;
// Roots returned by resolveReference are already parsed, no need to parse here
}
else if (docSource != null)
{
root = KMLRoot.create(docSource);
root.parse();
}
// If root is non-null we have succeeded in loading the document.
if (root != null)
{
// Schedule a callback on the EDT to let the BalloonController finish loading the document.
final KMLRoot pinnedRoot = root; // Final ref that can be accessed by anonymous class
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
BalloonController.this.onDocumentLoaded(docUrl, pinnedRoot, featureRef);
}
});
this.cancel();
}
}
catch (final Exception e)
{
// Schedule a callback on the EDT to report the error to the BalloonController
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
BalloonController.this.onDocumentFailed(docUrl, e);
}
});
this.cancel();
}
}
}
}