gov.nasa.worldwind.ogc.kml.KMLRoot Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.ogc.kml;
import gov.nasa.worldwind.WorldWind;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.event.Message;
import gov.nasa.worldwind.exception.*;
import gov.nasa.worldwind.ogc.kml.impl.*;
import gov.nasa.worldwind.ogc.kml.io.*;
import gov.nasa.worldwind.render.DrawContext;
import gov.nasa.worldwind.util.*;
import gov.nasa.worldwind.util.xml.*;
import javax.xml.stream.*;
import javax.xml.stream.events.XMLEvent;
import java.beans.PropertyChangeSupport;
import java.io.*;
import java.net.*;
import java.util.Map;
import java.util.zip.*;
/**
* Parses a KML or KMZ document and provides access to its contents. Instructions for parsing KML/KMZ files and streams
* are given in the Description section of {@link gov.nasa.worldwind.ogc.kml}.
*
* @author tag
* @version $Id: KMLRoot.java 1951 2014-04-20 18:57:50Z tgaskins $
*/
public class KMLRoot extends KMLAbstractObject implements KMLRenderable
{
/** Reference to the KMLDoc representing the KML or KMZ file. */
protected KMLDoc kmlDoc;
/** The event reader used to parse the document's XML. */
protected XMLEventReader eventReader;
/** The input stream underlying the event reader. */
protected InputStream eventStream;
/** The parser context for the document. */
protected KMLParserContext parserContext;
/**
* The PropertyChangeSupport
that receives property change events this KMLRoot listens for, and sends
* property change events to this KMLRoot's listeners. Lazily initialized in getChangeSupport
.
* Initially null
.
*/
protected PropertyChangeSupport propertyChangeSupport;
/**
* Indicates this KML root's detail hint. Modifies the default relationship of KML scene resolution to screen
* resolution as viewing distance changes. Values greater than 0 increase the resolution. Values less than 0
* decrease the resolution. Initially 0.
*/
protected double detailHint;
/** Flag to indicate that the feature has been fetched from the hash map. */
protected boolean featureFetched = false;
protected KMLAbstractFeature feature;
/** Flag to indicate that the network link control element has been fetched from the hash map. */
protected boolean linkControlFetched = false;
protected KMLNetworkLinkControl networkLinkControl;
protected AbsentResourceList absentResourceList = new AbsentResourceList();
/**
* Creates a KML root for an untyped source. The source must be either a {@link File}, a {@link URL}, a {@link
* InputStream}, or a {@link String} identifying either a file path or a URL. For all types other than
* InputStream
an attempt is made to determine whether the source is KML or KMZ; KML is assumed if the
* test is not definitive. Null is returned if the source type is not recognized.
*
* @param docSource either a {@link File}, a {@link URL}, or an {@link InputStream}, or a {@link String} identifying
* a file path or URL.
*
* @return a new {@link KMLRoot} for the specified source, or null if the source type is not supported.
*
* @throws IllegalArgumentException if the source is null.
* @throws IOException if an error occurs while reading the source.
*/
public static KMLRoot create(Object docSource) throws IOException
{
return create(docSource, true);
}
/**
* Creates a KML root for an untyped source. The source must be either a {@link File}, a {@link URL}, a {@link
* InputStream}, or a {@link String} identifying either a file path or a URL. For all types other than
* InputStream
an attempt is made to determine whether the source is KML or KMZ; KML is assumed if the
* test is not definitive. Null is returned if the source type is not recognized.
*
* @param docSource either a {@link File}, a {@link URL}, or an {@link InputStream}, or a {@link String}
* identifying a file path or URL.
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @return a new {@link KMLRoot} for the specified source, or null if the source type is not supported.
*
* @throws IllegalArgumentException if the source is null.
* @throws IOException if an error occurs while reading the source.
*/
public static KMLRoot create(Object docSource, boolean namespaceAware) throws IOException
{
if (docSource == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (docSource instanceof File)
return new KMLRoot((File) docSource, namespaceAware);
else if (docSource instanceof URL)
return new KMLRoot((URL) docSource, null, namespaceAware);
else if (docSource instanceof InputStream)
return new KMLRoot((InputStream) docSource, null, namespaceAware);
else if (docSource instanceof String)
{
File file = new File((String) docSource);
if (file.exists())
return new KMLRoot(file, namespaceAware);
URL url = WWIO.makeURL(docSource);
if (url != null)
return new KMLRoot(url, null, namespaceAware);
}
return null;
}
/**
* Creates a KML root for an untyped source and parses it. The source must be either a {@link File}, a {@link URL},
* a {@link InputStream}, or a {@link String} identifying either a file path or a URL. For all types other than
* InputStream
an attempt is made to determine whether the source is KML or KMZ; KML is assumed if the
* test is not definitive. Null is returned if the source type is not recognized.
*
* Note: Because there are so many incorrectly formed KML files in distribution, it's often not possible to parse
* with a namespace aware parser. This method first tries to use a namespace aware parser, but if a severe problem
* occurs during parsing, it will try again using a namespace unaware parser. Namespace unaware parsing typically
* bypasses many problems, but it also causes namespace qualified elements in the XML to be unrecognized.
*
* @param docSource either a {@link File}, a {@link URL}, or an {@link InputStream}, or a {@link String} identifying
* a file path or URL.
*
* @return a new {@link KMLRoot} for the specified source, or null if the source type is not supported.
*
* @throws IllegalArgumentException if the source is null.
* @throws IOException if an error occurs while reading the source.
* @throws javax.xml.stream.XMLStreamException
* if the KML file has severe errors.
*/
public static KMLRoot createAndParse(Object docSource) throws IOException, XMLStreamException
{
KMLRoot kmlRoot = KMLRoot.create(docSource);
if (kmlRoot == null)
{
String message = Logging.getMessage("generic.UnrecognizedSourceTypeOrUnavailableSource",
docSource.toString());
throw new IllegalArgumentException(message);
}
try
{
// Try with a namespace aware parser.
kmlRoot.parse();
}
catch (XMLStreamException e)
{
// Try without namespace awareness.
kmlRoot = KMLRoot.create(docSource, false);
kmlRoot.parse();
}
return kmlRoot;
}
/**
* Create a new KMLRoot
for a {@link KMLDoc} instance. A KMLDoc represents KML and KMZ files from
* either files or input streams.
*
* @param docSource the KMLDoc instance representing the KML document.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the KML document.
*/
public KMLRoot(KMLDoc docSource) throws IOException
{
this(docSource, true);
}
/**
* Create a new KMLRoot
for a {@link KMLDoc} instance. A KMLDoc represents KML and KMZ files from
* either files or input streams.
*
* @param docSource the KMLDoc instance representing the KML document.
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the KML document.
*/
public KMLRoot(KMLDoc docSource, boolean namespaceAware) throws IOException
{
super(KMLConstants.KML_NAMESPACE);
if (docSource == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.kmlDoc = docSource;
this.initialize(namespaceAware);
}
/**
* Create a new KMLRoot
for a {@link File}.
*
* @param docSource the File containing the document.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the KML document.
*/
public KMLRoot(File docSource) throws IOException
{
this(docSource, true);
}
/**
* Create a new KMLRoot
for a {@link File}.
*
* @param docSource the File containing the document.
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the KML document.
*/
public KMLRoot(File docSource, boolean namespaceAware) throws IOException
{
super(KMLConstants.KML_NAMESPACE);
if (docSource == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (WWIO.isContentType(docSource, KMLConstants.KML_MIME_TYPE))
this.kmlDoc = new KMLFile(docSource);
else if (WWIO.isContentType(docSource, KMLConstants.KMZ_MIME_TYPE))
{
try
{
this.kmlDoc = new KMZFile(docSource);
}
catch (ZipException e)
{
// We've encountered some zip files that will not open with ZipFile, but will open
// with ZipInputStream. Try again, this time opening treating the file as a stream.
// See WWJINT-282.
this.kmlDoc = new KMZInputStream(new FileInputStream(docSource));
}
}
else
throw new WWUnrecognizedException(Logging.getMessage("KML.UnrecognizedKMLFileType"));
this.initialize(namespaceAware);
}
/**
* Create a new KMLRoot
for an {@link InputStream}.
*
* @param docSource the input stream containing the document.
* @param contentType the content type of the stream data. Specify {@link KMLConstants#KML_MIME_TYPE} for plain KML
* and {@link KMLConstants#KMZ_MIME_TYPE} for KMZ. The content is treated as KML for any other
* value or a value of null.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the KML document.
*/
public KMLRoot(InputStream docSource, String contentType) throws IOException
{
this(docSource, contentType, true);
}
/**
* Create a new KMLRoot
for an {@link InputStream}.
*
* @param docSource the input stream containing the document.
* @param contentType the content type of the stream data. Specify {@link KMLConstants#KML_MIME_TYPE} for plain
* KML and {@link KMLConstants#KMZ_MIME_TYPE} for KMZ. The content is treated as KML for any
* other value or a value of null.
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the KML document.
*/
public KMLRoot(InputStream docSource, String contentType, boolean namespaceAware) throws IOException
{
super(KMLConstants.KML_NAMESPACE);
if (docSource == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (contentType != null && contentType.equals(KMLConstants.KMZ_MIME_TYPE))
this.kmlDoc = new KMZInputStream(docSource);
else if (contentType == null && docSource instanceof ZipInputStream)
this.kmlDoc = new KMZInputStream(docSource);
else
this.kmlDoc = new KMLInputStream(docSource, null);
this.initialize(namespaceAware);
}
/**
* Create a KMLRoot
for a {@link URL}.
*
* @param docSource the URL identifying the document.
* @param contentType the content type of the data. Specify {@link KMLConstants#KML_MIME_TYPE} for plain KML and
* {@link KMLConstants#KMZ_MIME_TYPE} for KMZ. Any other non-null value causes the content to be
* treated as plain KML. If null is specified the content type is read from the server or other
* end point of the URL. When a content type is specified, the content type returned by the URL's
* end point is ignored. You can therefore force the content to be treated as KML or KMZ
* regardless of what a server declares it to be.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the document.
*/
public KMLRoot(URL docSource, String contentType) throws IOException
{
this(docSource, contentType, true);
}
/**
* Create a KMLRoot
for a {@link URL}.
*
* @param docSource the URL identifying the document.
* @param contentType the content type of the data. Specify {@link KMLConstants#KML_MIME_TYPE} for plain KML and
* {@link KMLConstants#KMZ_MIME_TYPE} for KMZ. Any other non-null value causes the content to
* be treated as plain KML. If null is specified the content type is read from the server or
* other end point of the URL. When a content type is specified, the content type returned by
* the URL's end point is ignored. You can therefore force the content to be treated as KML or
* KMZ regardless of what a server declares it to be.
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @throws IllegalArgumentException if the document source is null.
* @throws IOException if an error occurs while reading the document.
*/
public KMLRoot(URL docSource, String contentType, boolean namespaceAware) throws IOException
{
super(KMLConstants.KML_NAMESPACE);
if (docSource == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
URLConnection conn = docSource.openConnection();
if (contentType == null)
contentType = conn.getContentType();
if (!(KMLConstants.KMZ_MIME_TYPE.equals(contentType) || KMLConstants.KML_MIME_TYPE.equals(contentType)))
contentType = WWIO.makeMimeTypeForSuffix(WWIO.getSuffix(docSource.getPath()));
if (KMLConstants.KMZ_MIME_TYPE.equals(contentType))
this.kmlDoc = new KMZInputStream(conn.getInputStream());
else
this.kmlDoc = new KMLInputStream(conn.getInputStream(), WWIO.makeURI(docSource));
this.initialize(namespaceAware);
}
/**
* Create a new KMLRoot
with a specific namespace. (The default namespace is defined by {@link
* gov.nasa.worldwind.ogc.kml.KMLConstants#KML_NAMESPACE}).
*
* @param namespaceURI the default namespace URI.
* @param docSource the KML source specified via a {@link KMLDoc} instance. A KMLDoc represents KML and KMZ files
* from either files or input streams.
*
* @throws IllegalArgumentException if the document source is null.
* @throws java.io.IOException if an I/O error occurs attempting to open the document source.
*/
public KMLRoot(String namespaceURI, KMLDoc docSource) throws IOException
{
this(namespaceURI, docSource, true);
}
/**
* Create a new KMLRoot
with a specific namespace. (The default namespace is defined by {@link
* gov.nasa.worldwind.ogc.kml.KMLConstants#KML_NAMESPACE}).
*
* @param namespaceURI the default namespace URI.
* @param docSource the KML source specified via a {@link KMLDoc} instance. A KMLDoc represents KML and KMZ
* files from either files or input streams.
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @throws IllegalArgumentException if the document source is null.
* @throws java.io.IOException if an I/O error occurs attempting to open the document source.
*/
public KMLRoot(String namespaceURI, KMLDoc docSource, boolean namespaceAware) throws IOException
{
super(namespaceURI);
if (docSource == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.kmlDoc = docSource;
this.initialize(namespaceAware);
}
/**
* Called just before the constructor returns. If overriding this method be sure to invoke
* super.initialize(boolean)
.
*
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @throws java.io.IOException if an I/O error occurs attempting to open the document source.
*/
protected void initialize(boolean namespaceAware) throws IOException
{
this.eventStream = this.getKMLDoc().getKMLStream();
this.eventReader = this.createReader(this.eventStream, namespaceAware);
if (this.eventReader == null)
throw new WWRuntimeException(Logging.getMessage("XML.UnableToOpenDocument", this.getKMLDoc()));
this.parserContext = this.createParserContext(this.eventReader);
}
/**
* Creates the event reader. Called from the constructor.
*
* @param docSource the document source to create a reader for. The type can be any of those supported by
* {@link WWXML#openEventReader(Object)}.
* @param namespaceAware specifies whether to use a namespace-aware XML parser. true
if so,
* false
if not.
*
* @return a new event reader, or null if the source type cannot be determined.
*/
protected XMLEventReader createReader(Object docSource, boolean namespaceAware)
{
return WWXML.openEventReader(docSource, namespaceAware);
}
/**
* Invoked during {@link #initialize(boolean)} to create the parser context. The parser context is created by the
* global {@link XMLEventParserContextFactory}.
*
* @param reader the reader to associate with the parser context.
*
* @return a new parser context.
*/
protected KMLParserContext createParserContext(XMLEventReader reader)
{
KMLParserContext ctx = (KMLParserContext)
XMLEventParserContextFactory.createParserContext(KMLConstants.KML_MIME_TYPE, this.getNamespaceURI());
if (ctx == null)
{
// Register a parser context for this root's default namespace
String[] mimeTypes = new String[] {KMLConstants.KML_MIME_TYPE, KMLConstants.KMZ_MIME_TYPE};
XMLEventParserContextFactory.addParserContext(mimeTypes, new KMLParserContext(this.getNamespaceURI()));
ctx = (KMLParserContext)
XMLEventParserContextFactory.createParserContext(KMLConstants.KML_MIME_TYPE, this.getNamespaceURI());
}
ctx.setEventReader(reader);
return ctx;
}
/**
* Specifies the object to receive notifications of important occurrences during parsing, such as exceptions and the
* occurrence of unrecognized element types.
*
* The default notification listener writes a message to the log, and otherwise does nothing.
*
* @param listener the listener to receive notifications. Specify null to indicate no listener.
*
* @see gov.nasa.worldwind.util.xml.XMLParserNotification
*/
public void setNotificationListener(final XMLParserNotificationListener listener)
{
if (listener == null)
{
this.parserContext.setNotificationListener(null);
}
else
{
this.parserContext.setNotificationListener(new XMLParserNotificationListener()
{
public void notify(XMLParserNotification notification)
{
// Set up so the user sees the notification coming from the root rather than the parser
notification.setSource(KMLRoot.this);
listener.notify(notification);
}
});
}
}
/**
* Returns the KML document for this KMLRoot
.
*
* @return the KML document for this root.
*/
public KMLDoc getKMLDoc()
{
return this.kmlDoc;
}
/**
* Finds a named element in the document.
*
* @param id the element's identifier. If null, null is returned.
*
* @return the element requested, or null if there is no corresponding element in the document.
*/
public Object getItemByID(String id)
{
return id != null ? this.getParserContext().getIdTable().get(id) : null;
}
public String getSupportFilePath(String link) throws IOException
{
return this.getKMLDoc().getSupportFilePath(link);
}
/**
* Resolves a reference to a remote or local element of the form address#identifier, where "address" identifies a
* local or remote document, including the current document, and and "identifier" is the id of the desired element.
*
* If the address part identifies the current document, the document is searched for the specified identifier.
* Otherwise the document is retrieved, opened and searched for the identifier. If the address refers to a remote
* document and the document has not previously been retrieved and cached locally, retrieval is initiated and this
* method returns null
. Once the document is successfully retrieved, subsequent calls to this method
* return the identified element, if it exists.
*
* If the link does not contain an identifier part, this initiates a retrieval for document referenced by the
* address part and returns null
. Once the document is retrieved this opens the the document as a
* KMLRoot
. Subsequent calls to this method return the opened document, if it exists.
*
* @param link the document address in the form address#identifier.
*
* @return the requested document, the requested or element within a document, or null
if the document
* or the element are not found.
*
* @throws IllegalArgumentException if the link
is null
.
*/
public Object resolveReference(String link)
{
if (link == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (absentResourceList.isResourceAbsent(link))
return null;
// Store remote files in the World Wind cache by default. This provides backward compatibility with applications
// depending on resolveReference's behavior prior to the addition of the cacheRemoteFile parameter.
Object o = this.resolveReference(link, true);
if (o == null)
absentResourceList.markResourceAbsent(link);
else
absentResourceList.unmarkResourceAbsent(link);
return o;
}
/**
* Resolves a reference to a remote or local element of the form address#identifier, where "address" identifies a
* local or remote document, including the current document, and and "identifier" is the id of the desired element.
*
* If the address part identifies the current document, the document is searched for the specified identifier.
* Otherwise the document is retrieved, opened and searched for the identifier. If the address refers to a remote
* document and the document has not previously been retrieved and cached locally, retrieval is initiated and this
* method returns null
. Once the document is successfully retrieved, subsequent calls to this method
* return the identified element, if it exists.
*
* If the link does not contain an identifier part, this initiates a retrieval for document referenced by the
* address part and returns null
. Once the document is retrieved this opens the the document as a
* KMLRoot
. Subsequent calls to this method return the opened document, if it exists.
*
* The cacheRemoteFile
parameter specifies whether to store a retrieved remote document in the World
* Wind cache or in a temporary location. This parameter has no effect if the document exists locally. The temporary
* location for a retrieved document does not persist between runtime sessions, and subsequent invocations of this
* method may not return the same temporary location.
*
* @param link the document address in the form address#identifier.
* @param cacheRemoteFile true
to store remote documents in the World Wind cache, or false
* to store remote documents in a temporary location. Has no effect if the address is a local
* document.
*
* @return the requested document, the requested or element within a document, or null
if the document
* or the element are not found.
*
* @throws IllegalArgumentException if the link
is null
.
*/
public Object resolveReference(String link, boolean cacheRemoteFile)
{
if (link == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
try
{
String[] linkParts = link.split("#");
String linkBase = linkParts[0];
String linkRef = linkParts.length > 1 ? linkParts[1] : null;
// See if it's a reference to an internal element.
if (WWUtil.isEmpty(linkBase) && !WWUtil.isEmpty(linkRef))
return this.getItemByID(linkRef);
// Interpret the path relative to the current document.
String path = this.getSupportFilePath(linkBase);
if (path == null)
path = linkBase;
// See if it's an already found and parsed KML file.
Object o = WorldWind.getSessionCache().get(path);
if (o != null && o instanceof KMLRoot)
return linkRef != null ? ((KMLRoot) o).getItemByID(linkRef) : o;
URL url = WWIO.makeURL(path);
if (url == null)
{
// See if the reference can be resolved to a local file.
o = this.resolveLocalReference(path, linkRef);
}
// If we didn't find a local file, treat it as a remote reference.
if (o == null)
o = this.resolveRemoteReference(path, linkRef, cacheRemoteFile);
if (o != null)
return o;
// If the reference was not resolved as a remote reference, look for a local element identified by the
// reference string. This handles the case of malformed internal references that omit the # sign at the
// beginning of the reference.
return this.getItemByID(link);
}
catch (Exception e)
{
String message = Logging.getMessage("generic.UnableToResolveReference", link);
Logging.logger().warning(message);
}
return null;
}
/**
* Resolves a reference to a local element identified by address and identifier, where {@code linkBase} identifies a
* document, including the current document, and {@code linkRef} is the id of the desired element.
*
* If {@code linkBase} refers to a local KML or KMZ file and {@code linkRef} is non-null, the return value is the
* element identified by {@code linkRef}. If {@code linkRef} is null, the return value is a parsed {@link KMLRoot}
* for the KML file identified by {@code linkBase}.
*
* If {@code linkBase} refers a local file that is not a KML or KMZ file then {@code linkBase} is returned. If
* {@code linkBase} cannot be resolved to a local file then null is returned.
*
* @param linkBase the address of the document containing the requested element.
* @param linkRef the element's identifier.
*
* @return the requested element, or null if the element is not found.
*
* @throws IllegalArgumentException if the address is null.
*/
public Object resolveLocalReference(String linkBase, String linkRef)
{
if (linkBase == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
try
{
File file = new File(linkBase);
if (!file.exists())
return null;
// Determine whether the file is a KML or KMZ. If it's not just return the original address.
if (!WWIO.isContentType(file, KMLConstants.KML_MIME_TYPE, KMLConstants.KMZ_MIME_TYPE))
return linkBase;
// Attempt to open and parse the KML/Z file, trying both namespace aware and namespace unaware stream
// readers if necessary.
KMLRoot refRoot = KMLRoot.createAndParse(file);
// An exception is thrown if parsing fails, so no need to check for null.
// Add the parsed file to the session cache so it doesn't have to be parsed again.
WorldWind.getSessionCache().put(linkBase, refRoot);
// Now check the newly opened KML/Z file for the referenced item, if a reference was specified.
if (linkRef != null)
return refRoot.getItemByID(linkRef);
else
return refRoot;
}
catch (Exception e)
{
String message = Logging.getMessage("generic.UnableToResolveReference", linkBase + "/" + linkRef);
Logging.logger().warning(message);
return null;
}
}
/**
* Resolves a reference to a remote element identified by address and identifier, where {@code linkBase} identifies
* a remote document, and {@code linkRef} is the id of the desired element. This method retrieves resources
* asynchronously using the {@link gov.nasa.worldwind.cache.FileStore}.
*
* The return value is null if the file is not yet available in the FileStore. If {@code linkBase} refers to a KML
* or KMZ file and {@code linkRef} is non-null, the return value is the element identified by {@code linkRef}. If
* {@code linkBase} refers to a KML or KMZ and {@code linkRef} is null, the return value is a parsed {@link KMLRoot}
* for the KML file identified by {@code linkBase}. Otherwise the return value is a {@link URL} to the file in the
* file cache.
*
* @param linkBase the address of the document containing the requested element.
* @param linkRef the element's identifier.
*
* @return URL to the requested file, parsed KMLRoot, or KML feature. Returns null if the document is not yet
* available in the FileStore.
*
* @throws IllegalArgumentException if the {@code linkBase} is null.
*/
public Object resolveRemoteReference(String linkBase, String linkRef)
{
if (linkBase == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
// Store remote files in the World Wind cache by default. This provides backward compatibility with applications
// depending on resolveRemoteReference's behavior prior to the addition of the cacheRemoteFile parameter.
return this.resolveRemoteReference(linkBase, linkRef, true);
}
/**
* Resolves a reference to a remote element identified by address and identifier, where {@code linkBase} identifies
* a remote document, and {@code linkRef} is the id of the desired element. This method retrieves resources
* asynchronously using the {@link gov.nasa.worldwind.cache.FileStore}.
*
* The return value is null if the file is not yet available in the FileStore. If {@code linkBase} refers to a KML
* or KMZ file and {@code linkRef} is non-null, the return value is the element identified by {@code linkRef}. If
* {@code linkBase} refers to a KML or KMZ and {@code linkRef} is null, the return value is a parsed {@link KMLRoot}
* for the KML file identified by {@code linkBase}. Otherwise the return value is a {@link URL} to the file in the
* file cache or a temporary location, depending on the value of cacheRemoteFile
.
*
* The cacheRemoteFile
parameter specifies whether to store a retrieved remote file in the World Wind
* cache or in a temporary location. This parameter has no effect if the file exists locally. The temporary location
* for a retrieved file does not persist between runtime sessions, and subsequent invocations of this method may not
* return the same temporary location.
*
* @param linkBase the address of the document containing the requested element.
* @param linkRef the element's identifier.
* @param cacheRemoteFile true
to store remote files in the World Wind cache, or false
to
* store remote files in a temporary location. Has no effect if the address is a local file.
*
* @return URL to the requested file, parsed KMLRoot, or KML feature. Returns null if the document is not yet
* available in the FileStore.
*
* @throws IllegalArgumentException if the {@code linkBase} is null.
*/
public Object resolveRemoteReference(String linkBase, String linkRef, boolean cacheRemoteFile)
{
if (linkBase == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
try
{
// See if it's in the cache. If not, requestFile will start another thread to retrieve it and return null.
URL url = WorldWind.getDataFileStore().requestFile(linkBase, cacheRemoteFile);
if (url == null)
return null;
// It's in the cache. If it's a KML/Z, try to parse it so we can search for the specified reference. If it's
// not KML/Z, just return the url for the cached file.
String contentType = WorldWind.getDataFileStore().getContentType(linkBase);
if (contentType == null)
{
String suffix = WWIO.getSuffix(linkBase.split(";")[0]); // strip of trailing garbage
if (!WWUtil.isEmpty(suffix))
contentType = WWIO.makeMimeTypeForSuffix(suffix);
}
if (!this.canParseContentType(contentType))
return url;
// If the file is a KML or KMZ document, attempt to open it. We can't open it as a File with createAndParse
// because the KMLRoot that will be created needs to have the remote address in order to resolve any
// relative references within it, so we have to implement the namespace-aware/namespace-unaware attempts
// here.
KMLRoot refRoot;
try
{
// Try to parse with a namespace-aware event stream.
refRoot = this.parseCachedKMLFile(url, linkBase, contentType, true);
}
catch (XMLStreamException e)
{
// Well that didn't work, so try with a namespace-unaware event stream. If this attempt fails this
// method logs the exception and returns null.
refRoot = this.parseCachedKMLFile(url, linkBase, contentType, false);
}
// If the file could not be parsed as KML, then just return the URL.
if (refRoot == null)
return url;
// Add the parsed file to the session cache so it doesn't have to be parsed again.
WorldWind.getSessionCache().put(linkBase, refRoot);
// Now check the newly opened KML/Z file for the referenced item, if a reference was specified.
if (linkRef != null)
return refRoot.getItemByID(linkRef);
else
return refRoot;
}
catch (Exception e)
{
String message = Logging.getMessage("generic.UnableToResolveReference", linkBase + "/" + linkRef);
Logging.logger().warning(message);
return null;
}
}
/**
* Resolves a NetworkLink to a local or remote KML document. This method retrieves remote resources asynchronously
* using the {@link gov.nasa.worldwind.cache.FileStore}.
*
* The return value is a parsed KMLRoot representing the linked document. The return value is null if the linked
* file is not a KML file, or is not yet available in the FileStore.
*
* The cacheRemoteFile
parameter specifies whether to store a retrieved remote file in the World Wind
* cache or in a temporary location. This parameter has no effect if the file exists locally. The temporary location
* for a retrieved file does not persist between runtime sessions, and subsequent invocations of this method may not
* return the same temporary location.
*
* @param link the address to resolve
* @param cacheRemoteFile true
to store remote files in the World Wind cache, or false
to
* store remote files in a temporary location. Has no effect if the address is a local file.
* @param updateTime the time at which the link was last updated. If a cached file exists for the specified
* resource, the file must have been retrieved after the link update time. Otherwise, the
* cache entry is considered invalid, and the file is deleted and retrieved again.
*
* @return URL to the requested file, parsed KMLRoot, or KML feature. Returns null if the document is not yet
* available in the FileStore.
*
* @throws IllegalArgumentException if the {@code link} is null.
*/
public Object resolveNetworkLink(String link, boolean cacheRemoteFile, long updateTime)
{
if (link == null)
{
String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
Object o = null;
try
{
// Interpret the path relative to the current document.
String path = this.getSupportFilePath(link);
if (path == null)
path = link;
// If the file is eligible for caching, check the session cache to see if it has already been retrieved and
// parsed.
if (cacheRemoteFile)
{
o = WorldWind.getSessionCache().get(path);
if (o instanceof KMLRoot)
return o;
}
URL url = WWIO.makeURL(path);
if (url == null)
{
// See if the reference can be resolved to a local file.
o = this.resolveLocalReference(path, null);
}
// If we didn't find a local file, treat it as a remote reference.
if (o == null)
{
url = WorldWind.getDataFileStore().requestFile(path, cacheRemoteFile);
if (url != null)
{
// Check the file's modification time against the link update time. If the file was last modified
// earlier than the link update time then we need to remove the cached file from the file store,
// and start a new file retrieval.
File file = new File(url.toURI());
if (file.lastModified() < updateTime)
{
WorldWind.getDataFileStore().removeFile(link);
}
}
// Call resolveRemoteReference to retrieve and parse the file.
o = this.resolveRemoteReference(path, null, cacheRemoteFile);
}
}
catch (Exception e)
{
String message = Logging.getMessage("generic.UnableToResolveReference", link);
Logging.logger().warning(message);
}
return o;
}
/**
* Check a cached resource for expiration. If the resource is expired, evict it from the cache.
*
* @param link Link that identifies the resource to check for expiration. This is the same link that was
* passed to resolveReference to retrieve the resource.
* @param expirationTime Time at which the resource expires, in milliseconds since the Epoch. If the current system
* time is greater than the expiration time, then the resource will be evicted.
*/
public void evictIfExpired(String link, long expirationTime)
{
try
{
URL url = WorldWind.getDataFileStore().requestFile(link, false);
if (url != null)
{
// Check the file's modification time against the link update time. If the file was last modified
// earlier than the link update time then we need to remove the cached file from the file store,
// and start a new file retrieval.
File file = new File(url.toURI());
if (file.lastModified() < expirationTime)
WorldWind.getDataFileStore().removeFile(link);
}
}
catch (URISyntaxException e)
{
String message = Logging.getMessage("generic.UnableToResolveReference", link);
Logging.logger().warning(message);
}
}
/**
* Returns the expiration time of a file retrieved by {@link #resolveReference(String) resolveReference} or {@link
* #resolveNetworkLink(String, boolean, long) resolveNetworkLink}.
*
* @param link the address of the file (the same address as was previously passed to resolveReference). If null,
* zero is returned.
*
* @return The expiration time of the file, in milliseconds since the Epoch (January 1, 1970, 00:00:00 GMT). Zero
* indicates that there is no expiration time. Returns zero if te resource identified by {@code link} has
* not been retrieved.
*/
public long getExpiration(String link)
{
try
{
if (link == null)
return 0;
// Interpret the path relative to the current document.
String path = this.getSupportFilePath(link);
if (path == null)
path = link;
return WorldWind.getDataFileStore().getExpirationTime(path);
}
catch (IOException e)
{
String message = Logging.getMessage("generic.UnableToResolveReference", link);
Logging.logger().warning(message);
}
return 0;
}
/**
* Determines if a MIME type can be parsed as KML or KMZ. Parsable types are the KML and KMZ MIME types, as well as
* "text/plain" and "text/xml".
*
* @param mimeType Type to test. May be null.
*
* @return {@code true} if {@code mimeType} can be parsed as KML.
*/
protected boolean canParseContentType(String mimeType)
{
return KMLConstants.KML_MIME_TYPE.equals(mimeType) || KMLConstants.KMZ_MIME_TYPE.equals(mimeType)
|| "text/plain".equals(mimeType) || "text/xml".equals(mimeType);
}
/**
* Open and parse the specified file expressed as a file: URL..
*
* @param url the URL of the file to open, expressed as a URL with a scheme of "file".
* @param linkBase the original address of the document if the file is a retrieved and cached file.
* @param contentType the mime type of the file's content, either a KML or KMZ mime type.
* @param namespaceAware specifies whether to use a namespace aware event reader.
*
* @return A {@code KMLRoot} representing the file's KML contents.
*
* @throws IOException if an I/O error occurs during opening and parsing.
* @throws XMLStreamException if a server parsing error is encountered.
*/
protected KMLRoot parseCachedKMLFile(URL url, String linkBase, String contentType, boolean namespaceAware)
throws IOException, XMLStreamException
{
KMLDoc kmlDoc;
InputStream refStream = url.openStream();
if (KMLConstants.KMZ_MIME_TYPE.equals(contentType))
kmlDoc = new KMZInputStream(refStream);
else // Attempt to parse as KML
kmlDoc = new KMLInputStream(refStream, WWIO.makeURI(linkBase));
try
{
KMLRoot refRoot = new KMLRoot(kmlDoc, namespaceAware);
refRoot = refRoot.parse(); // also closes the URL's stream
return refRoot;
}
catch (XMLStreamException e)
{
refStream.close(); // parsing failed, so explicitly close the stream
throw e;
}
}
/**
* Starts document parsing. This method initiates parsing of the KML document and returns when the full document has
* been parsed.
*
* @param args optional arguments to pass to parsers of sub-elements.
*
* @return this
if parsing is successful, otherwise null.
*
* @throws javax.xml.stream.XMLStreamException
* if an exception occurs while attempting to read the event stream.
*/
public KMLRoot parse(Object... args) throws XMLStreamException
{
KMLParserContext ctx = this.parserContext;
try
{
for (XMLEvent event = ctx.nextEvent(); ctx.hasNext(); event = ctx.nextEvent())
{
if (event == null)
continue;
// Allow a element in any namespace
if (event.isStartElement() && event.asStartElement().getName().getLocalPart().equals("kml"))
{
super.parse(ctx, event, args);
return this;
}
// Allow the document to start without a element. There are many such files around.
else if (event.isStartElement() && ctx.getParser(event) != null)
{
this.doParseEventContent(ctx, event, args);
return this;
}
}
}
finally
{
ctx.getEventReader().close();
this.closeEventStream();
}
return null;
}
/** Closes the event stream associated with this context's XML event reader. */
protected void closeEventStream()
{
try
{
this.eventStream.close();
this.eventStream = null;
}
catch (IOException e)
{
String message = Logging.getMessage("generic.ExceptionClosingXmlEventReader");
Logging.logger().warning(message);
}
}
protected XMLEventParserContext getParserContext()
{
return this.parserContext;
}
/**
* Returns the hint
attribute of the KML
element (the document root).
*
* @return the hint attribute, or null if the attribute is not specified.
*/
public String getHint()
{
return (String) this.getField("hint");
}
/**
* Returns the {@link gov.nasa.worldwind.ogc.kml.KMLNetworkLinkControl} element if the document root contains it.
*
* @return the element if it is specified in the document, otherwise null.
*/
public KMLNetworkLinkControl getNetworkLinkControl()
{
if (!linkControlFetched)
{
this.networkLinkControl = (KMLNetworkLinkControl) this.getField("NetworkLinkControl");
this.linkControlFetched = true;
}
return this.networkLinkControl;
}
/**
* Returns the KML Feature
element contained in the document root.
*
* @return the feature element if it is specified in the document, otherwise null.
*/
public KMLAbstractFeature getFeature()
{
if (!this.featureFetched)
{
this.feature = findFeature();
this.featureFetched = true;
}
return this.feature;
}
/**
* Searches this root's fields for the KML Feature element.
*
* @return the feature element, or null if none was found.
*/
protected KMLAbstractFeature findFeature()
{
if (!this.hasFields())
return null;
for (Map.Entry entry : this.getFields().getEntries())
{
if (entry.getValue() instanceof KMLAbstractFeature)
return (KMLAbstractFeature) entry.getValue();
}
return null;
}
/**
* Indicates this KML root's detail hint, which is described in {@link #setDetailHint(double)}
.
*
* @return the detail hint.
*
* @see #setDetailHint(double)
*/
public double getDetailHint()
{
return this.detailHint;
}
/**
* Specifies this KML root's detail hint. The detail hint modifies the default relationship of KML scene resolution
* to screen resolution as the viewing distance changes. Values greater than 0 cause KML elements with a level of
* detail to appear at higher resolution at greater distances than normal, but at an increased performance cost.
* Values less than 0 decrease the default resolution at any given distance. The default value is 0. Values
* typically range between -0.5 and 0.5.
*
* The top level KML root's detail hint is inherited by all KML elements beneath that root, including any descendant
* KML roots loaded by network links. If this KML root has been loaded by a network link, its detail hint is
* ignored.
*
* @param detailHint the degree to modify the default relationship of KML scene resolution to screen resolution as
* viewing distance changes. Values greater than 0 increase the resolution. Values less than 0
* decrease the resolution. The default value is 0.
*/
public void setDetailHint(double detailHint)
{
this.detailHint = detailHint;
}
/** Request any scene containing this KML document be repainted. */
public void requestRedraw()
{
this.firePropertyChange(AVKey.REPAINT, null, null);
}
public void preRender(KMLTraversalContext tc, DrawContext dc)
{
if (this.getFeature() != null)
this.getFeature().preRender(tc, dc);
}
public void render(KMLTraversalContext tc, DrawContext dc)
{
if (this.getFeature() != null)
this.getFeature().render(tc, dc);
}
//**********************************************************************
//********************* Property change support ************************
//**********************************************************************
/**
* {@inheritDoc}
*
* Overridden to forward the message to the root feature.
*
* @param msg The message that was received.
*/
@Override
public void onMessage(Message msg)
{
if (this.getFeature() != null)
this.getFeature().onMessage(msg);
}
/**
* Adds the specified property change listener that will be called for all list changes.
*
* @param listener the listener to call.
*
* @throws IllegalArgumentException if listener
is null
* @see java.beans.PropertyChangeSupport
*/
public void addPropertyChangeListener(java.beans.PropertyChangeListener listener)
{
if (listener == null)
{
String msg = Logging.getMessage("nullValue.ListenerIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.getChangeSupport().addPropertyChangeListener(listener);
}
/**
* Removes the specified property change listener.
*
* @param listener the listener to remove.
*
* @throws IllegalArgumentException if listener
is null.
* @see java.beans.PropertyChangeSupport
*/
public void removePropertyChangeListener(java.beans.PropertyChangeListener listener)
{
if (listener == null)
{
String msg = Logging.getMessage("nullValue.ListenerIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.getChangeSupport().addPropertyChangeListener(listener);
}
/**
* Fire a property change event.
*
* @param propertyChangeEvent Event to fire.
*/
public void firePropertyChange(java.beans.PropertyChangeEvent propertyChangeEvent)
{
if (propertyChangeEvent == null)
{
String msg = Logging.getMessage("nullValue.PropertyChangeEventIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.getChangeSupport().firePropertyChange(propertyChangeEvent);
}
/**
* Fire a property change event.
*
* @param propertyName Name of the property change changed.
* @param oldValue The previous value of the property.
* @param newValue The new value of the property.
*/
public void firePropertyChange(String propertyName, Object oldValue, Object newValue)
{
if (propertyName == null)
{
String msg = Logging.getMessage("nullValue.PropertyNameIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.getChangeSupport().firePropertyChange(propertyName, oldValue, newValue);
}
/**
* Get the PropertyChangeSupport object for this KML object. The support object will be created if it does not
* already exist.
*
* @return PropertyChangeSupport for this KML object.
*/
protected synchronized PropertyChangeSupport getChangeSupport()
{
if (this.propertyChangeSupport == null)
this.propertyChangeSupport = new PropertyChangeSupport(this);
return this.propertyChangeSupport;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy