blob: cbc89068b9846f428cebe5fcd4f0821ae6049ae3 [file] [log] [blame]
// PlatformLatLongDataProvider.java
package org.eclipse.stem.definitions.adapters.spatial.geo;
/*******************************************************************************
* Copyright (c) 2006 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.core.runtime.Preferences.IPropertyChangeListener;
import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.ecore.resource.impl.URIConverterImpl;
import org.eclipse.stem.definitions.Activator;
import org.eclipse.stem.definitions.adapters.spatial.geo.LatLong.SegmentBuilder;
import org.eclipse.stem.definitions.adapters.spatial.geo.preferences.PreferenceConstants;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* This class interprets a URI that specifies a file of (i.e., "platform")
* lat/long data in some format (currently <a
* href="http://www.opengis.net/gml/">GML</a>).
*
* @see InlineLatLongDataProvider
* @see LatLongProviderAdapter
*/
@SuppressWarnings("deprecation")
public class PlatformLatLongDataProvider implements LatLongDataProvider {
/**
* The class name of the first SAX XML parser to attempt to create if the
* default cannot be found. Value:
* <code>org.apache.xerces.parsers.SAXParser</code>
*/
public static final String SAX_PARSER1 = "org.apache.xerces.parsers.SAXParser";
/**
* The class name of the second SAX XML parser to attempt to create if the
* default cannot be found. Value:
* <code>org.apache.crimson.parser.XMLReaderImpl</code>
*/
public static final String SAX_PARSER2 = "org.apache.crimson.parser.XMLReaderImpl";
/**
* This set of nested Maps implements a cache for latitude/longitude data
* read in. The key of the first map is the URI of the file containing the
* data. The key of the second map is the identifier that is the fragment of
* the original query URI. If the first map does not contain an entry for a
* specified file, then one is created and populated with the contents of
* the file.
*/
static Map<URI, Map<String, LatLong>> cache = new ConcurrentHashMap<URI, Map<String, LatLong>>();
/**
* This is the scheme for a "platform" data URI that specifies a file that
* contains lat/long data.
*/
public static final String PLATFORM_SCHEME = "platform";
/**
* This is the name of the folder that contains the normal resolution GML
* files.
*/
private static final CharSequence NORMAL_RES_FOLDER = "country";
/**
* This is the name of the folder that contains the lower resolution GML
* files
*/
private static final CharSequence LOW_RES_FOLDER = "country_reduced";
/**
* If <code>true</code> then the latitude/longitude points will be sampled
* and some will be discarded to reduce their number.
*/
public boolean isSampling = false;
/**
* This is the frequency at which the latitude/longitude data points will be
* sampled. A value of "1" means every point will be included, a value of
* "2" means that every 2nd point will be included. The first and last data
* points are always included.
*/
public int sampleFrequency = 1;
/**
* If <code>true</code> then I/O Exceptions are logged, otherwise they are
* ignored.
*/
public boolean reportIOExceptions = true;
/**
* If <code>true</code> then the path to the GML file will be modified to
* point to a smaller GML file that has a lower resolution.
*/
public boolean useLowerResolutionLatLongData = false;
/**
* Constructor
*/
public PlatformLatLongDataProvider() {
super();
setPreferences();
Activator.getDefault().getPluginPreferences()
.addPropertyChangeListener(new IPropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent event) {
setPreferences();
// Flush everything in the cache to force it to be read
// again from the gml file and re-sampled
cache = new ConcurrentHashMap<URI, Map<String, LatLong>>();
} // propertyChange
} // IPropertyChangeListener
);
} // PlatformLatLongDataProvider
public void flush()
{
cache = new ConcurrentHashMap<URI, Map<String, LatLong>>();
}
/**
* @param latlongFileURI
* @return the lat/long values
*/
public LatLong getLatLong(final URI latlongFileURI) {
LatLong retValue = null;
// The URI should have an ID as the fragment that specifies the
// latitude/longitude data in the GML file.
// Is there an entry for our key?
retValue = getLatLongMap(latlongFileURI.trimFragment()).get(
latlongFileURI.fragment());
retValue = retValue != null ? retValue : new LatLong();
return retValue;
} // getLatLong
/**
* This method is just like {@link #getLatLong(URI)} except that it will
* immediately return an "empty" LatLong instance if the data for the URI
* has not been retrieved. It will also start a separate job to retrieve
* that data and will return it when that job completes successfully.
*
* @param latlongFileURI
* @return the lat/long values
*/
public LatLong getLatLongNoWait(final URI latlongFileURI) {
LatLong retValue = null;
// The URI should have an ID as the fragment that specifies the
// latitude/longitude data in the GML file.
// Is there an entry for our key?
retValue = getLatLongMapNoWait(latlongFileURI.trimFragment()).get(
latlongFileURI.fragment());
retValue = retValue != null ? retValue : new LatLong();
return retValue;
} // getLatLong
/**
* @param fileURI
* the uri of a file of latitude/longitude data
* @return the identifiers of the latitude/longitude data in the file
*/
public Set<String> getIds(final URI fileURI) {
return getLatLongMap(fileURI).keySet();
} // getIds
/**
* This method is just like {@link #getIds(URI)} except that it will return
* an empty set if the data referenced by the URI has not been read yet. It
* will then cause a separate job to start that will read that data. A
* subsequent call, after that job completes successfully, would return a
* set of ids.
*
* @param fileURI
* the uri of a file of latitude/longitude data
* @return the identifiers of the latitude/longitude data in the file
*/
public Set<String> getIdsNoWait(final URI fileURI) {
return getLatLongMapNoWait(fileURI).keySet();
} // getIds
/**
* @param fileURI
* the uri of a file of latitude/longitude data
* @return the map between id's and data values for that file.
*/
private Map<String, LatLong> getLatLongMap(final URI fileURI) {
// Have we processed this file before?
Map<String, LatLong> latLongMap = cache.get(fileURI);
if (latLongMap == null) {
// No
// We need to read the contents of the file and populate the entry
latLongMap = createLatLongMap(fileURI);
cache.put(fileURI, latLongMap);
}
return latLongMap;
} // getLatLongMap
/**
* This method is the same as {@link #getLatLongMap(URI)}, except that it
* immediately returns an instance of the lat/long map. This map will be
* empty if this is the first time a request has been made for it. In that
* case a separate Job (i.e., thread) is scheduled to read and populate the
* map from a GML file. When that is finished, the empty map is replaced by
* the populated one so that the next call will return the populated map.
*
* @param fileURI
* the uri of a file of latitude/longitude data
* @return the map between id's and data values for that file.
*/
synchronized private Map<String, LatLong> getLatLongMapNoWait(
final URI fileURI) {
// Have we processed this file before?
Map<String, LatLong> latLongMap = cache.get(fileURI);
if (latLongMap == null) {
// No
// We need to put an empty map into the cache so that the next call
// doesn't try to start up another job to read in the file.
latLongMap = new ConcurrentHashMap<String, LatLong>();
cache.put(fileURI, latLongMap);
// Now down to business, we create a separate job to read in the
// lat/long data from the GML file and populate the map.
final Job readGMLJob = new Job(fileURI.toString()) {
/**
* @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
protected IStatus run(IProgressMonitor monitor) {
monitor.beginTask(fileURI.toString(), 100);
// We need to read the contents of the file and populate the
// entry
final Map<String, LatLong> latLongMap = createLatLongMap(fileURI);
// Replace the empty map with this one.
cache.put(fileURI, latLongMap);
monitor.done();
return new Status(IStatus.OK, Activator.PLUGIN_ID,
IStatus.OK, "Done", null);
} // run
}; // readGMLJob
readGMLJob.schedule();
} // if no lat/long map
return latLongMap;
} // getLatLongMap
Map<String, LatLong> createLatLongMap(final URI fileURI) {
final Map<String, LatLong> latLongMap = new ConcurrentHashMap<String, LatLong>();
final URIConverter converter = new URIConverterImpl();
final GMLDecoder gMLDecoder = new GMLDecoder(latLongMap);
try {
final InputStream is = converter
.createInputStream(adjustFilePath(fileURI));
final BufferedReader in = new BufferedReader(new InputStreamReader(
is));
parseInputStream(in, gMLDecoder);
} // try
catch (final IOException e) {
// It could be that we tried to get a low resolution version of the
// file and it wasn't found. So let's try getting the regular
// resolution
// Are we getting low resolution files?
if (useLowerResolutionLatLongData) {
// Yes
try {
// Try again without adjusting the file path
final InputStream is = converter.createInputStream(fileURI);
final BufferedReader in = new BufferedReader(
new InputStreamReader(is));
parseInputStream(in, gMLDecoder);
} catch (final FileNotFoundException fnfe) {
// Should we report the missing file?
if (reportIOExceptions) {
// Yes
Activator.logInformation(fnfe.getMessage(), fnfe);
}
} // catch FileNotFoundException
catch (final IOException e2) {
// Should we report the error?
if (reportIOExceptions) {
// Yes
Activator.logError(e2.getMessage(), e2);
}
} // catch IOException
} // if using low resolution
// Should we report the error?
else if (reportIOExceptions) {
// Yes
Activator.logError(e.getMessage(), e);
}
} // catch
return latLongMap;
} // createLatLongMap
/**
* This method determines if it should modify the path to the GML file so
* that a different file is selected. Typically, this is to switch to a
* lower resolution GML file.
*
* @param fileURI
* the original URI
* @return the adjusted URI
*/
private URI adjustFilePath(final URI fileURI) {
URI retValue = fileURI;
// Should we use a lower resolution version of the file?
if (useLowerResolutionLatLongData) {
// Yes
final String adjustedPath = fileURI.toString().replace(
NORMAL_RES_FOLDER, LOW_RES_FOLDER);
retValue = URI.createURI(adjustedPath);
}
return retValue;
} // adjustFilePath
/**
* Set the preferences.
*
* @see #isSampling
* @see #sampleFrequency
*/
private void setPreferences() {
final Preferences preferences = Activator.getDefault()
.getPluginPreferences();
if(preferences !=null) {
isSampling = preferences.getBoolean(PreferenceConstants.DOWN_SAMPLE_LAT_LONG_DATA_BOOLEAN_PREFERENCE);
sampleFrequency = preferences.getInt(PreferenceConstants.LAT_LONG_SAMPLE_FREQUENCY_INTEGER_PREFERENCE);
reportIOExceptions = preferences.getBoolean(PreferenceConstants.REPORT_IO_EXCEPTIONS_BOOLEAN_PREFERENCE);
useLowerResolutionLatLongData = preferences.getBoolean(PreferenceConstants.USE_LOWER_RESOLUTION_LAT_LONG_DATA_BOOLEAN_PREFERENCE);
}
} // setPerferences
/**
* Try to create a SAX Parser. The method first attempts to create the
* default parser. The class name of this parser is assigned to the property
* "org.xml.sax.driver". This value is defined on the command line that
* starts the Java interpreter
* <code>java -Dorg.xml.sax.driver=org.apache.xerces.parsers.SAXParser</code>
* If it is not successful in finding this parser, it will attempt to create
* an instance of <code>org.apache.xerces.parsers.SAXParser</code>, if
* that fails, it will try the parser
* <code>"org.apache.crimson.parser.XMLReaderImpl</code>. If that fails,
* it will throw a <code>SimulationException</code> reporting the
* inability to create a SAX XML Parser.
*
* @return a SAXParser
*/
public static XMLReader getSAXParser() {
XMLReader retValue = null;
try {
// Try to make the default SAX Parser
retValue = XMLReaderFactory.createXMLReader();
} // try
catch (final SAXException saxe) {
// Try specific parsers
try { // SAX_PARSER1
retValue = XMLReaderFactory.createXMLReader(SAX_PARSER1);
} // try SAX_PARSER1
catch (final SAXException saxe1) {
try { // try SAX_PARSER2
retValue = XMLReaderFactory.createXMLReader(SAX_PARSER2);
} // try SAX_PARSER2
catch (final SAXException saxe2) {
Activator.logError(saxe2.getMessage(), saxe2);
} // catch SAX_PARSER2
} // catch SAX_PARSER1
} // catch default
return retValue;
} // getSAXParser()
/**
* Parse the specified input character stream using the
*
* specified Handler. Where the output is stored depends on the handler
*
* @param in
* BufferedReader to read the XML from.
* @param handler
* The class that will handle the parsing
*/
public static void parseInputStream(final BufferedReader in,
final DefaultHandler handler) {
try {
final XMLReader saxParser = getSAXParser();
saxParser.setContentHandler(handler);
saxParser.parse(new InputSource(in));
in.close();
} catch (final IOException e) {
Activator.logError(e.getMessage(), e);
} catch (final SAXException e) {
Activator.logError(e.getMessage(), e);
}
} // parseInputStream
/**
* This class is a SAX Handler used to process the contents of a GML file
* specifying latitude/longitude data.
*/
public class GMLDecoder extends DefaultHandler {
// XML element name
private static final String XML_MAP_ELEMENT_NAME = "Map";
private static final String XML_TITLE_ELEMENT_NAME = "title";
private static final String XML_SUBTITLE_ELEMENT_NAME = "subTitle";
private static final String XML_UPDATED_ELEMENT_NAME = "updated";
private static final String XML_ENTRY_ELEMENT_NAME = "entry";
private static final String XML_WHERE_ELEMENT_NAME = "where";
private static final String XML_OUTER_BOUNDARY_ELEMENT_NAME = "outerBoundaryIs";
private static final String XML_LINEAR_RING_ELEMENT_NAME = "LinearRing";
private static final String XML_POLYGON_ELEMENT_NAME = "Polygon";
private static final String XML_POSLIST_ELEMENT_NAME = "posList";
private static final String XML_ID_ATTR = "gml:id";
/**
* This is the title parsed from the file
*/
private String title = "";
/**
* This is the sub-title parsed from the file;
*/
// We're ignoring subtitles right now
// private String subTitle = "";
/**
* This is the date the file was updated as parsed from the file;
*/
// We're ignoring updated right now
// private String updated = "";
/**
* This is the identifier of the current polygon being parsed
*/
private String currentPolygonId = null;
// Parse position in the document being parsed
private Locator locator = null;
/**
* The map that is constructed during the processing of the GML file.
*/
Map<String, LatLong> latLongMap = null;
/**
* This is the collection of
* {@link org.eclipse.stem.definitions.adapters.spatial.geo.LatLong.Segment}'s
* collected for a polygon.
*/
private LatLong polygonLatLong = null;
/**
* This segment builder is used to collect together related
* latitude/longitude data pairs to create a
* {@link org.eclipse.stem.definitions.adapters.spatial.geo.LatLong.Segment}
*/
private LatLong.SegmentBuilder segmentBuilder = null;
/**
* These are the characters that have been collected but are not yet
* complete.
*/
private StringBuilder characterStringBuilder = new StringBuilder();
/**
* Constructor
*/
protected GMLDecoder(final Map<String, LatLong> latLongMap) {
super();
this.latLongMap = latLongMap;
} // GMLDecoder
/**
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String,
* java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
@Override
public void startElement(final String namespace, final String sName, final String qName,
final Attributes attributes) throws SAXException {
// The name of the element
final String elementName = sName.equals("") ? qName : sName;
// The "Map" root element?
if (elementName.equalsIgnoreCase(XML_MAP_ELEMENT_NAME)) {
// Yes
assert latLongMap != null;
} // else if Map
// A title element?
else if (elementName.equalsIgnoreCase(XML_TITLE_ELEMENT_NAME)) {
// Yes
characterStringBuilder = new StringBuilder();
} // title
// subTitle element?
else if (elementName.equalsIgnoreCase(XML_SUBTITLE_ELEMENT_NAME)) {
// Yes
characterStringBuilder = new StringBuilder();
} // subTitle
// updated element?
else if (elementName.equalsIgnoreCase(XML_UPDATED_ELEMENT_NAME)) {
// Yes
characterStringBuilder = new StringBuilder();
} // updated
// entry element?
else if (elementName.equalsIgnoreCase(XML_ENTRY_ELEMENT_NAME)) {
// Yes
} // updated
// where element?
else if (elementName.equalsIgnoreCase(XML_WHERE_ELEMENT_NAME)) {
// Yes
} // where
// outerBoundaryIs element?
else if (elementName
.equalsIgnoreCase(XML_OUTER_BOUNDARY_ELEMENT_NAME)) {
// Yes
} // outerBoundaryIs
// linearRing element?
else if (elementName.equalsIgnoreCase(XML_LINEAR_RING_ELEMENT_NAME)) {
// Yes
} // linearRing
// polygon element?
else if (elementName.equalsIgnoreCase(XML_POLYGON_ELEMENT_NAME)) {
// Yes
assert currentPolygonId == null;
assert polygonLatLong == null;
currentPolygonId = attributes.getValue(XML_ID_ATTR);
polygonLatLong = new LatLong();
} // polygon
// posList element?
else if (elementName.equalsIgnoreCase(XML_POSLIST_ELEMENT_NAME)) {
// Yes
assert segmentBuilder == null;
segmentBuilder = new SegmentBuilder();
characterStringBuilder = new StringBuilder();
} // posList
else {
// No
// We have no idea what this element is
throw new SAXParseException("Invalid element name \""
+ elementName + "\"", locator);
} // else unrecognized element name
} // startElement
/**
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String,
* java.lang.String, java.lang.String)
*/
@Override
public void endElement(final String namespace, final String sName, final String qName)
throws SAXException {
// The name of the element
final String elementName = sName.equals("") ? qName : sName;
// The "Map" root element?
if (elementName.equalsIgnoreCase(XML_MAP_ELEMENT_NAME)) {
// Yes
assert latLongMap != null;
} // else if Map
// A title element?
else if (elementName.equalsIgnoreCase(XML_TITLE_ELEMENT_NAME)) {
// Yes
title = characterStringBuilder.toString();
characterStringBuilder = new StringBuilder();
} // title
// subTitle element?
else if (elementName.equalsIgnoreCase(XML_SUBTITLE_ELEMENT_NAME)) {
// Yes
// We're ignoring subtitles right now
// subTitle = characterStringBuilder.toString();
characterStringBuilder = new StringBuilder();
} // subTitle
// updated element?
else if (elementName.equalsIgnoreCase(XML_UPDATED_ELEMENT_NAME)) {
// Yes
// updated = characterStringBuilder.toString();
characterStringBuilder = new StringBuilder();
} // updated
// entry element?
else if (elementName.equalsIgnoreCase(XML_ENTRY_ELEMENT_NAME)) {
// Yes
} // entry
// where element?
else if (elementName.equalsIgnoreCase(XML_WHERE_ELEMENT_NAME)) {
// Yes
} // where
// outerBoundaryIs element?
else if (elementName
.equalsIgnoreCase(XML_OUTER_BOUNDARY_ELEMENT_NAME)) {
// Yes
} // outerBoundaryIs
// linearRing element?
else if (elementName.equalsIgnoreCase(XML_LINEAR_RING_ELEMENT_NAME)) {
// Yes
} // linearRing
// polygon element?
else if (elementName.equalsIgnoreCase(XML_POLYGON_ELEMENT_NAME)) {
// Yes
latLongMap.put(currentPolygonId, polygonLatLong);
currentPolygonId = null;
polygonLatLong = null;
} // polygon
// posList element?
else if (elementName.equalsIgnoreCase(XML_POSLIST_ELEMENT_NAME)) {
// Yes
// Parse the current characters for the latitude/longitude data
// pairs
final StringTokenizer st = new StringTokenizer(
characterStringBuilder.toString(), " \n");
characterStringBuilder = new StringBuilder();
while (st.hasMoreTokens()) {
final String value1 = st.nextToken();
// Is there a matching Longitude?
if (st.hasMoreTokens()) {
// Yes
// Lat
final String value2 = st.nextToken();
segmentBuilder.add(value1, value2);
} else {
// No
// lat/long mismatch
Activator
.logError(
"Latitude/Longitude data titled \""
+ title
+ "\" is missing a longitude match for the latitude value \""
+ value1 + "\" at line: "
+ locator.getLineNumber()
+ ", column: "
+ locator.getColumnNumber(),
null);
// discard whatever we've collected so far
segmentBuilder.clear();
break;
} // else
} // while more tokens
// Anything in the segment?
if (segmentBuilder.size() > 0) {
// Yes
try {
polygonLatLong.add(isSampling ? segmentBuilder
.toSegment(sampleFrequency) : segmentBuilder
.toSegment());
} catch (IllegalArgumentException iae) {
Activator.logError(
"Problem processing Latitude/Longitude data titled \""
+ title + "\"", iae);
}
} // if
segmentBuilder = null;
} // posList
else {
// No
// We have no idea what this element is
throw new SAXParseException("Invalid element name \""
+ elementName + "\"", locator);
} // else unrecognized element name
} // endElement
/**
* @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
*/
@Override
public void characters(final char[] ch, final int start,
final int length) {
characterStringBuilder.append(new String(ch, start, length));
} // characters
/**
* @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException)
*/
@Override
public void warning(final SAXParseException spe) {
Activator.logInformation(spe.getMessage(), spe);
} // warning
/**
* @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException)
*/
@Override
public void error(final SAXParseException spe) {
Activator.logError(spe.getMessage(), spe);
} // error
/**
* @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException)
*/
@Override
public void fatalError(final SAXParseException spe) {
Activator.logError(spe.getMessage(), spe);
} // fatalError
/**
* @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator)
*/
@Override
public void setDocumentLocator(final Locator locator) {
this.locator = locator;
} // setDocumentLocator
} // GMLDecoder
} // PlatformLatLongDataProvider