src.gov.nasa.worldwind.ogc.kml.impl.KMLAbstractBalloon Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwindx Show documentation
Show all versions of worldwindx 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.worldwind.ogc.kml.impl;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.ogc.kml.*;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.util.*;
import gov.nasa.worldwind.util.webview.WebResourceResolver;
import java.awt.*;
import java.beans.*;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.*;
/**
* An implementation of {@link Balloon} that applies a {@link KMLBalloonStyle} to the balloon. Rather than fully
* implementing the Balloon interface, this class provides a thin wrapper around another Balloon implementation and adds
* the logic for styling the Balloon according to the KML style. All Balloon methods on this class pass through to the
* contained Balloon.
*
* To use KML Balloon, first create a Balloon of the desired type, and then create the KML Balloon. For example:
*
*
*
* KMLPlacemark myPlacemark = ...;
* Position placemarkPosition = ...;
*
* // Create a BrowserBalloon for the placemark.
* GlobeBalloon globeBalloon = new GlobeBrowserBalloon(myPlacemark.getName(), placemarkPosition);
*
* // Create a KML Balloon to apply the placemark's KML BalloonStyle to the browser balloon.
* KMLGlobeBalloonImpl kmlBalloon = new KMLGlobeBalloonImpl(globeBalloon, myPlacemark);
*
*
*
* @author pabercrombie
* @version $Id: KMLAbstractBalloon.java 1555 2013-08-20 13:33:12Z pabercrombie $
*/
public abstract class KMLAbstractBalloon implements Balloon, WebResourceResolver, PropertyChangeListener
{
public static final String DISPLAY_MODE_HIDE = "hide";
public static final String DISPLAY_MODE_DEFAULT = "default";
protected KMLAbstractFeature parent;
protected String displayMode = DISPLAY_MODE_DEFAULT;
/** Indicates that the balloon has default text loaded, rather than text supplied by the BalloonStyle. */
protected boolean usingDefaultText;
protected boolean normalAttributesResolved;
protected boolean highlightAttributesResolved;
/** Text when balloon is not highlighted. */
protected String normalText;
/** Text when balloon is highlighted. */
protected String highlightText;
/**
* Create a globe attached Balloon Impl object for a KML feature.
*
* @param feature Feature to create balloon annotation for.
*/
public KMLAbstractBalloon(KMLAbstractFeature feature)
{
if (feature == null)
{
String msg = Logging.getMessage("nullValue.FeatureIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.parent = feature;
}
/**
* Initialize the object.
*
* @param balloon The balloon contained in this wrapper object.
*/
protected void initialize(Balloon balloon)
{
balloon.setTextDecoder(this.createTextDecoder(this.parent));
balloon.setValue(AVKey.CONTEXT, this.parent);
// Configure this balloon to resolve relative paths in the KML balloon HTML via its resolve() method.
if (balloon instanceof AbstractBrowserBalloon)
{
((AbstractBrowserBalloon) balloon).setResourceResolver(this);
}
// Listen for balloon property changes. These will be forwarded to the parent KMLRoot so that the balloon
// can trigger a repaint when its contents have changed.
balloon.addPropertyChangeListener(this);
}
/**
* Get the Balloon object that is contained in the KMLBalloon object.
*
* @return The balloon contained by this object.
*/
protected abstract Balloon getBalloon();
/**
* Render the balloon. This method will attempt to resolve the balloon style, if it has not already been resolved.
*
* @param dc Draw context
*/
public void render(DrawContext dc)
{
Balloon balloon = this.getBalloon();
if (balloon.isHighlighted() && !this.highlightAttributesResolved)
{
this.makeAttributesCurrent(KMLConstants.HIGHLIGHT);
}
else if (!this.normalAttributesResolved)
{
this.makeAttributesCurrent(KMLConstants.NORMAL);
}
this.determineActiveText();
if (!WWUtil.isEmpty(this.getText()) && !DISPLAY_MODE_HIDE.equals(this.getDisplayMode()))
balloon.render(dc);
}
/** Determine the balloon text for this frame, depending on the balloon highlight state. */
protected void determineActiveText()
{
String activeText = null;
// If the balloon is highlighted, use the highlight text.
if (this.isHighlighted())
{
activeText = this.highlightText;
}
// If the balloon is not highlighted, or there is no highlight text, use the normal text.
if (activeText == null)
{
activeText = this.normalText;
}
// Set the text if it does not match the active text.
if (activeText != null && !activeText.equals(this.getText()))
{
this.setText(activeText);
}
}
/**
* Update the balloon attributes to match the KML BalloonStyle.
*
* @param attrType Type of attributes to update. Either {@link KMLConstants#NORMAL} or {@link
* KMLConstants#HIGHLIGHT}.
*/
protected void makeAttributesCurrent(String attrType)
{
BalloonAttributes attrs = this.getInitialBalloonAttributes();
KMLBalloonStyle balloonStyle = (KMLBalloonStyle) this.parent.getSubStyle(new KMLBalloonStyle(null), attrType);
String displayMode = balloonStyle.getDisplayMode();
if (displayMode != null)
this.setDisplayMode(displayMode);
this.assembleBalloonAttributes(balloonStyle, attrs);
if (balloonStyle.hasField(AVKey.UNRESOLVED))
attrs.setUnresolved(true);
else
attrs.setUnresolved(false);
if (KMLConstants.NORMAL.equals(attrType))
{
this.getBalloon().setAttributes(attrs);
// Set balloon text. If the style does not provide text, set the default text, if it has not been set
// already. We use a field to track if the default text has been set to avoid continually resetting default
// text if the style cannot be resolved.
String text = balloonStyle.getText();
if (text != null)
{
if (this.mustAddHyperlinks(text))
text = this.addHyperlinks(text);
this.getBalloon().setText(text);
this.normalText = text;
}
else if (!this.usingDefaultText)
{
text = this.createDefaultBalloonText();
if (this.mustAddHyperlinks(text))
text = this.addHyperlinks(text);
this.getBalloon().setText(text);
this.usingDefaultText = true;
this.normalText = text;
}
if (!attrs.isUnresolved() || !balloonStyle.hasFields())
this.normalAttributesResolved = true;
}
else
{
this.getBalloon().setHighlightAttributes(attrs);
String text = balloonStyle.getText();
if (this.mustAddHyperlinks(text))
text = this.addHyperlinks(text);
this.highlightText = text;
if (!attrs.isUnresolved() || !balloonStyle.hasFields())
this.highlightAttributesResolved = true;
}
}
/**
* Build a default balloon text string for the feature.
*
* @return Default balloon text.
*/
protected String createDefaultBalloonText()
{
StringBuilder sb = new StringBuilder();
// Create default text for features that have a description
String name = this.parent.getName();
String description = this.parent.getDescription();
if (!WWUtil.isEmpty(name))
sb.append("").append(name).append("");
if (!WWUtil.isEmpty(description))
sb.append("
").append(description);
KMLExtendedData extendedData = this.parent.getExtendedData();
if (extendedData != null)
{
List data = extendedData.getData();
if (data != null && !data.isEmpty())
{
this.createDefaultExtendedDataText(sb, data);
}
List schemaData = extendedData.getSchemaData();
if (schemaData != null && !schemaData.isEmpty())
{
this.createDefaultSchemaDataText(sb, schemaData);
}
}
return sb.toString();
}
/**
* Build a default balloon text string for the feature's extended data. This implementation builds a simple data
* table.
*
* @param sb Extended data string will be appended to this StringBuilder.
* @param data The feature's extended data.
*/
protected void createDefaultExtendedDataText(StringBuilder sb, List data)
{
sb.append("");
for (KMLData item : data)
{
String value = item.getValue();
if (!WWUtil.isEmpty(value))
{
String name = item.getName() != null ? item.getName() : "";
sb.append("$[").append(name).append("/displayName] ").append(value).append(
" ");
}
}
sb.append("
");
}
/**
* Build a default balloon text string for the feature's schema data. This implementation builds a simple data
* table.
*
* @param sb Extended data string will be appended to this StringBuilder.
* @param data The feature's schema data.
*/
protected void createDefaultSchemaDataText(StringBuilder sb, List data)
{
sb.append("");
for (KMLSchemaData schemaData : data)
{
KMLSchema schema = (KMLSchema) this.parent.getRoot().resolveReference(schemaData.getSchemaUrl());
for (KMLSimpleData simpleData : schemaData.getSimpleData())
{
String value = simpleData.getCharacters();
if (!WWUtil.isEmpty(value))
{
String dataName = simpleData.getName() != null ? simpleData.getName() : "";
sb.append("");
// Insert the schema name, if the schema can be resolved. Otherwise just use the data name.
if (schema != null && !WWUtil.isEmpty(schema.getName()) && !WWUtil.isEmpty(dataName))
{
sb.append("$[").append(schema.getName()).append("/").append(dataName).append("/displayName]");
}
else
{
sb.append(dataName);
}
sb.append(" ").append(value).append(" ");
}
}
}
sb.append("
");
}
/**
* Determines if URLs in the balloon text should be converted to hyperlinks. The Google KML specification states the
* GE will add hyperlinks to balloon text that does not contain HTML formatting. This method searches for a
* <html> tag in the content to determine if the content is HTML or plain text.
*
* @param text Balloon text to process.
*
* @return True if URLs should be converted links. Returns true if a <html> tag is found in the text.
*/
protected boolean mustAddHyperlinks(String text)
{
return text != null
&& !text.contains("
* This method is conservative about what is identified as a URL, in order to avoid adding links to text that the
* user did not intend to be linked. Only HTTP and HTTPS URLs are recognised, as well as text that begins with www.
* (in which case a http:// prefix will be prepended). Some punctuation characters that are valid URL characters
* (such as parentheses) are not treated as URL characters here because users may expect the punctuation to separate
* the URL from text.
*
* @param text Text to process. Each URL in the text will be replaced with <a href="url" target="_blank"> url
* </a>
*
* @return Text with hyperlinks added.
*/
protected String addHyperlinks(String text)
{
// Regular expression to match a http(s) URL, or an entire anchor tag. Note that this does not match all valid
// URLs. It is designed to match obvious URLs that occur in KML balloons, with minimal chance of matching text
// the user did not intend to be a link.
String regex =
"" // Match all text between anchor tags
+ "|" // or
+ "[^'\"]" // Non-quote (avoids matching quoted urls in code)
+ "(" // Capture group 1
+ "(?:https?://|www\\.)" // HTTP(S) protocol or www. (non-capturing group)
+ "[a-z0-9.$%+/_-]+" // Match until a non-URL character
+ ")";
StringBuffer sb = new StringBuffer();
Matcher matcher = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.DOTALL).matcher(text);
while (matcher.find())
{
// If the match is a URL then group 1 holds the matched URL. If group 1 is null then the match is an anchor
// tag, in which case we just skip it to avoid adding links to text that is already part of a link.
String url = matcher.group(1);
if (url != null)
{
String prefix = url.toLowerCase().startsWith("www") ? "http://" : "";
matcher.appendReplacement(sb, "$1");
}
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* Get the default attributes applied to the balloon. These attributes will be modified by {@link
* #assembleBalloonAttributes(gov.nasa.worldwind.ogc.kml.KMLBalloonStyle, gov.nasa.worldwind.render.BalloonAttributes)
* assembleBalloonAttributes} to reflect the settings in the KML BalloonStyle.
*
* @return Initial balloon attributes.
*/
protected BalloonAttributes getInitialBalloonAttributes()
{
BalloonAttributes attrs;
if (this.isHighlighted())
{
attrs = this.getHighlightAttributes();
// Copy the normal attributes if there are no highlight attributes
if (attrs == null && this.getAttributes() != null)
{
attrs = new BasicBalloonAttributes(this.getAttributes());
}
}
else
{
attrs = this.getAttributes();
}
if (attrs == null)
attrs = new BasicBalloonAttributes();
return attrs;
}
/**
* Apply a KML BalloonStyle to the balloon attributes object.
*
* @param style KML style to apply.
* @param balloonAttributes Attributes to modify.
*/
protected void assembleBalloonAttributes(KMLBalloonStyle style, BalloonAttributes balloonAttributes)
{
// Attempt to use the bgColor property. This is the preferred method for encoding a BalloonStyle's background
// color since KML 2.1, therefore we give it priority.
String bgColor = style.getBgColor();
// If the bgColor property is null, attempt to use the deprecated color property. color was deprecated in
// KML 2.1, but must be supported for backward compatibility. See the KML 2.1 reference, section 7.1.3.
if (bgColor == null)
bgColor = style.getColor();
if (bgColor != null)
balloonAttributes.setInteriorMaterial(new Material(WWUtil.decodeColorABGR(bgColor)));
String textColor = style.getTextColor();
if (textColor != null)
balloonAttributes.setTextColor(WWUtil.decodeColorABGR(textColor));
}
/**
* Create the text decoder that will process the text in the balloon.
*
* @param feature Feature to decode text for.
*
* @return New text decoder.
*/
protected TextDecoder createTextDecoder(KMLAbstractFeature feature)
{
return new KMLBalloonTextDecoder(feature);
}
/**
* Get the balloon display mode, either {@link #DISPLAY_MODE_DEFAULT} or {@link #DISPLAY_MODE_HIDE}.
*
* @return The current display mode.
*
* @see #setDisplayMode(String)
*/
public String getDisplayMode()
{
return this.displayMode;
}
/**
* Set the balloon's display mode, either {@link #DISPLAY_MODE_DEFAULT} or {@link #DISPLAY_MODE_HIDE}. When the mode
* is {@link #DISPLAY_MODE_HIDE}, the balloon will not be drawn.
*
* @param displayMode New display mode.
*
* @see #getDisplayMode()
*/
public void setDisplayMode(String displayMode)
{
if (displayMode == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.displayMode = displayMode;
}
/**
* {@inheritDoc}
*
* This implementation resolves relative resource paths by calling {@link
* gov.nasa.worldwind.ogc.kml.io.KMLDoc#getSupportFilePath(String)}
on the parent
* KMLAbstractFeature's
KMLDoc
. This is necessary to correctly resolve relative references
* in a KMZ archive.
*
* This returns null
if the specified address
is null
.
*/
public URL resolve(String address)
{
if (address == null)
return null;
try
{
// Resolve the relative path against the KMLDoc, and convert it to a URL. We use makeURL variant that
// accepts a default protocol, because we know the path is an absolute file path. If the path does not
// define a valid URL, makeURL returns null and the balloon treats this as an unresolved resource.
String absolutePath = this.parent.getRoot().getKMLDoc().getSupportFilePath(address);
if (!WWUtil.isEmpty(absolutePath))
{
File file = new File(absolutePath);
return file.toURI().toURL();
}
}
catch (IOException e)
{
Logging.logger().log(Level.WARNING, Logging.getMessage("KML.UnableToResolvePath", address), e.getMessage());
}
return null;
}
/**
* Forward property change events to the parent KMLRoot.
*
* @param evt Event to forward.
*/
public void propertyChange(PropertyChangeEvent evt)
{
this.parent.getRoot().firePropertyChange(evt);
}
//***************************************************************************//
//********************** Balloon implementation ***************************//
//**************************************************************************//
/** {@inheritDoc}. This method passes through to the contained balloon. */
public boolean isHighlighted()
{
return this.getBalloon().isHighlighted();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setHighlighted(boolean highlighted)
{
this.getBalloon().setHighlighted(highlighted);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public boolean isAlwaysOnTop()
{
return this.getBalloon().isAlwaysOnTop();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setAlwaysOnTop(boolean alwaysOnTop)
{
this.getBalloon().setAlwaysOnTop(alwaysOnTop);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public boolean isPickEnabled()
{
return this.getBalloon().isPickEnabled();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setPickEnabled(boolean enable)
{
this.getBalloon().setPickEnabled(enable);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public String getText()
{
return this.getBalloon().getText();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setText(String text)
{
this.getBalloon().setText(text);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public BalloonAttributes getAttributes()
{
return this.getBalloon().getAttributes();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setAttributes(BalloonAttributes attrs)
{
this.getBalloon().setAttributes(attrs);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public BalloonAttributes getHighlightAttributes()
{
return this.getBalloon().getHighlightAttributes();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setHighlightAttributes(BalloonAttributes attrs)
{
this.getBalloon().setHighlightAttributes(attrs);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public TextDecoder getTextDecoder()
{
return this.getBalloon().getTextDecoder();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setTextDecoder(TextDecoder decoder)
{
this.getBalloon().setTextDecoder(decoder);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public Object getDelegateOwner()
{
return this.getBalloon().getDelegateOwner();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setDelegateOwner(Object owner)
{
this.getBalloon().setDelegateOwner(owner);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public boolean isVisible()
{
return this.getBalloon().isVisible();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setVisible(boolean visible)
{
this.getBalloon().setVisible(visible);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public Rectangle getBounds(DrawContext dc)
{
return this.getBalloon().getBounds(dc);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public double getMinActiveAltitude()
{
return this.getBalloon().getMinActiveAltitude();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setMinActiveAltitude(double minActiveAltitude)
{
this.getBalloon().setMinActiveAltitude(minActiveAltitude);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public double getMaxActiveAltitude()
{
return this.getBalloon().getMaxActiveAltitude();
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public void setMaxActiveAltitude(double maxActiveAltitude)
{
this.getBalloon().setMaxActiveAltitude(maxActiveAltitude);
}
//***************************************************************************//
//********************** AVList implementation ***************************//
//**************************************************************************//
/** {@inheritDoc}. This method passes through to the contained balloon. */
public Object setValue(String key, Object value)
{
return this.getBalloon().setValue(key, value);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public AVList setValues(AVList avList)
{
return this.getBalloon().setValues(avList);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public Object getValue(String key)
{
return this.getBalloon().getValue(key);
}
/** {@inheritDoc}. This method passes through to the contained balloon. */
public Collection