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

src.gov.nasa.worldwindx.examples.util.BalloonController Maven / Gradle / Ivy

Go to download

World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.

There is a newer version: 2.0.0-986
Show 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.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 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 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(); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy