// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.io;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import javax.xml.parsers.ParserConfigurationException;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.gpx.Extensions;
import org.openstreetmap.josm.data.gpx.GpxConstants;
import org.openstreetmap.josm.data.gpx.GpxData;
import org.openstreetmap.josm.data.gpx.GpxLink;
import org.openstreetmap.josm.data.gpx.GpxRoute;
import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
import org.openstreetmap.josm.data.gpx.WayPoint;
import org.openstreetmap.josm.tools.Utils;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Read a gpx file.
*
* Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.
* Both GPX version 1.0 and 1.1 are supported.
*
* @author imi, ramack
*/
public class GpxReader implements GpxConstants {
private enum State {
INIT,
GPX,
METADATA,
WPT,
RTE,
TRK,
EXT,
AUTHOR,
LINK,
TRKSEG,
COPYRIGHT
}
private String version;
/** The resulting gpx data */
private GpxData gpxData;
private final InputSource inputSource;
private class Parser extends DefaultHandler {
private GpxData data;
private Collection> currentTrack;
private Map currentTrackAttr;
private Collection currentTrackSeg;
private GpxRoute currentRoute;
private WayPoint currentWayPoint;
private State currentState = State.INIT;
private GpxLink currentLink;
private Extensions currentExtensions;
private Stack states;
private final Stack elements = new Stack<>();
private StringBuilder accumulator = new StringBuilder();
private boolean nokiaSportsTrackerBug;
@Override
public void startDocument() {
accumulator = new StringBuilder();
states = new Stack<>();
data = new GpxData();
}
private double parseCoord(String s) {
try {
return Double.parseDouble(s);
} catch (NumberFormatException ex) {
return Double.NaN;
}
}
private LatLon parseLatLon(Attributes atts) {
return new LatLon(
parseCoord(atts.getValue("lat")),
parseCoord(atts.getValue("lon")));
}
@Override
public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
elements.push(localName);
switch(currentState) {
case INIT:
states.push(currentState);
currentState = State.GPX;
data.creator = atts.getValue("creator");
version = atts.getValue("version");
if (version != null && version.startsWith("1.0")) {
version = "1.0";
} else if (!"1.1".equals(version)) {
// unknown version, assume 1.1
version = "1.1";
}
break;
case GPX:
switch (localName) {
case "metadata":
states.push(currentState);
currentState = State.METADATA;
break;
case "wpt":
states.push(currentState);
currentState = State.WPT;
currentWayPoint = new WayPoint(parseLatLon(atts));
break;
case "rte":
states.push(currentState);
currentState = State.RTE;
currentRoute = new GpxRoute();
break;
case "trk":
states.push(currentState);
currentState = State.TRK;
currentTrack = new ArrayList<>();
currentTrackAttr = new HashMap<>();
break;
case "extensions":
states.push(currentState);
currentState = State.EXT;
currentExtensions = new Extensions();
break;
case "gpx":
if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) {
nokiaSportsTrackerBug = true;
}
break;
default: // Do nothing
}
break;
case METADATA:
switch (localName) {
case "author":
states.push(currentState);
currentState = State.AUTHOR;
break;
case "extensions":
states.push(currentState);
currentState = State.EXT;
currentExtensions = new Extensions();
break;
case "copyright":
states.push(currentState);
currentState = State.COPYRIGHT;
data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author"));
break;
case "link":
states.push(currentState);
currentState = State.LINK;
currentLink = new GpxLink(atts.getValue("href"));
break;
case "bounds":
data.put(META_BOUNDS, new Bounds(
parseCoord(atts.getValue("minlat")),
parseCoord(atts.getValue("minlon")),
parseCoord(atts.getValue("maxlat")),
parseCoord(atts.getValue("maxlon"))));
break;
default: // Do nothing
}
break;
case AUTHOR:
switch (localName) {
case "link":
states.push(currentState);
currentState = State.LINK;
currentLink = new GpxLink(atts.getValue("href"));
break;
case "email":
data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain"));
break;
default: // Do nothing
}
break;
case TRK:
switch (localName) {
case "trkseg":
states.push(currentState);
currentState = State.TRKSEG;
currentTrackSeg = new ArrayList<>();
break;
case "link":
states.push(currentState);
currentState = State.LINK;
currentLink = new GpxLink(atts.getValue("href"));
break;
case "extensions":
states.push(currentState);
currentState = State.EXT;
currentExtensions = new Extensions();
break;
default: // Do nothing
}
break;
case TRKSEG:
if ("trkpt".equals(localName)) {
states.push(currentState);
currentState = State.WPT;
currentWayPoint = new WayPoint(parseLatLon(atts));
}
break;
case WPT:
switch (localName) {
case "link":
states.push(currentState);
currentState = State.LINK;
currentLink = new GpxLink(atts.getValue("href"));
break;
case "extensions":
states.push(currentState);
currentState = State.EXT;
currentExtensions = new Extensions();
break;
default: // Do nothing
}
break;
case RTE:
switch (localName) {
case "link":
states.push(currentState);
currentState = State.LINK;
currentLink = new GpxLink(atts.getValue("href"));
break;
case "rtept":
states.push(currentState);
currentState = State.WPT;
currentWayPoint = new WayPoint(parseLatLon(atts));
break;
case "extensions":
states.push(currentState);
currentState = State.EXT;
currentExtensions = new Extensions();
break;
default: // Do nothing
}
break;
default: // Do nothing
}
accumulator.setLength(0);
}
@Override
public void characters(char[] ch, int start, int length) {
/**
* Remove illegal characters generated by the Nokia Sports Tracker device.
* Don't do this crude substitution for all files, since it would destroy
* certain unicode characters.
*/
if (nokiaSportsTrackerBug) {
for (int i = 0; i < ch.length; ++i) {
if (ch[i] == 1) {
ch[i] = 32;
}
}
nokiaSportsTrackerBug = false;
}
accumulator.append(ch, start, length);
}
private Map getAttr() {
switch (currentState) {
case RTE: return currentRoute.attr;
case METADATA: return data.attr;
case WPT: return currentWayPoint.attr;
case TRK: return currentTrackAttr;
default: return null;
}
}
@SuppressWarnings("unchecked")
@Override
public void endElement(String namespaceURI, String localName, String qName) {
elements.pop();
switch (currentState) {
case GPX: // GPX 1.0
case METADATA: // GPX 1.1
switch (localName) {
case "name":
data.put(META_NAME, accumulator.toString());
break;
case "desc":
data.put(META_DESC, accumulator.toString());
break;
case "time":
data.put(META_TIME, accumulator.toString());
break;
case "keywords":
data.put(META_KEYWORDS, accumulator.toString());
break;
case "author":
if ("1.0".equals(version)) {
// author is a string in 1.0, but complex element in 1.1
data.put(META_AUTHOR_NAME, accumulator.toString());
}
break;
case "email":
if ("1.0".equals(version)) {
data.put(META_AUTHOR_EMAIL, accumulator.toString());
}
break;
case "url":
case "urlname":
data.put(localName, accumulator.toString());
break;
case "metadata":
case "gpx":
if ((currentState == State.METADATA && "metadata".equals(localName)) ||
(currentState == State.GPX && "gpx".equals(localName))) {
convertUrlToLink(data.attr);
if (currentExtensions != null && !currentExtensions.isEmpty()) {
data.put(META_EXTENSIONS, currentExtensions);
}
currentState = states.pop();
}
break;
case "bounds":
// do nothing, has been parsed on startElement
break;
default:
//TODO: parse extensions
}
break;
case AUTHOR:
switch (localName) {
case "author":
currentState = states.pop();
break;
case "name":
data.put(META_AUTHOR_NAME, accumulator.toString());
break;
case "email":
// do nothing, has been parsed on startElement
break;
case "link":
data.put(META_AUTHOR_LINK, currentLink);
break;
default: // Do nothing
}
break;
case COPYRIGHT:
switch (localName) {
case "copyright":
currentState = states.pop();
break;
case "year":
data.put(META_COPYRIGHT_YEAR, accumulator.toString());
break;
case "license":
data.put(META_COPYRIGHT_LICENSE, accumulator.toString());
break;
default: // Do nothing
}
break;
case LINK:
switch (localName) {
case "text":
currentLink.text = accumulator.toString();
break;
case "type":
currentLink.type = accumulator.toString();
break;
case "link":
if (currentLink.uri == null && accumulator != null && !accumulator.toString().isEmpty()) {
currentLink = new GpxLink(accumulator.toString());
}
currentState = states.pop();
break;
default: // Do nothing
}
if (currentState == State.AUTHOR) {
data.put(META_AUTHOR_LINK, currentLink);
} else if (currentState != State.LINK) {
Map attr = getAttr();
if (attr != null && !attr.containsKey(META_LINKS)) {
attr.put(META_LINKS, new LinkedList());
}
if (attr != null)
((Collection) attr.get(META_LINKS)).add(currentLink);
}
break;
case WPT:
switch (localName) {
case "ele":
case "magvar":
case "name":
case "src":
case "geoidheight":
case "type":
case "sym":
case "url":
case "urlname":
currentWayPoint.put(localName, accumulator.toString());
break;
case "hdop":
case "vdop":
case "pdop":
try {
currentWayPoint.put(localName, Float.valueOf(accumulator.toString()));
} catch (NumberFormatException e) {
currentWayPoint.put(localName, 0f);
}
break;
case "time":
case "cmt":
case "desc":
currentWayPoint.put(localName, accumulator.toString());
currentWayPoint.setTime();
break;
case "rtept":
currentState = states.pop();
convertUrlToLink(currentWayPoint.attr);
currentRoute.routePoints.add(currentWayPoint);
break;
case "trkpt":
currentState = states.pop();
convertUrlToLink(currentWayPoint.attr);
currentTrackSeg.add(currentWayPoint);
break;
case "wpt":
currentState = states.pop();
convertUrlToLink(currentWayPoint.attr);
if (currentExtensions != null && !currentExtensions.isEmpty()) {
currentWayPoint.put(META_EXTENSIONS, currentExtensions);
}
data.waypoints.add(currentWayPoint);
break;
default: // Do nothing
}
break;
case TRKSEG:
if ("trkseg".equals(localName)) {
currentState = states.pop();
currentTrack.add(currentTrackSeg);
}
break;
case TRK:
switch (localName) {
case "trk":
currentState = states.pop();
convertUrlToLink(currentTrackAttr);
data.tracks.add(new ImmutableGpxTrack(currentTrack, currentTrackAttr));
break;
case "name":
case "cmt":
case "desc":
case "src":
case "type":
case "number":
case "url":
case "urlname":
currentTrackAttr.put(localName, accumulator.toString());
break;
default: // Do nothing
}
break;
case EXT:
if ("extensions".equals(localName)) {
currentState = states.pop();
} else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) {
// only interested in extensions written by JOSM
currentExtensions.put(localName, accumulator.toString());
}
break;
default:
switch (localName) {
case "wpt":
currentState = states.pop();
break;
case "rte":
currentState = states.pop();
convertUrlToLink(currentRoute.attr);
data.routes.add(currentRoute);
break;
default: // Do nothing
}
}
}
@Override
public void endDocument() throws SAXException {
if (!states.empty())
throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
Extensions metaExt = (Extensions) data.get(META_EXTENSIONS);
if (metaExt != null && "true".equals(metaExt.get("from-server"))) {
data.fromServer = true;
}
gpxData = data;
}
/**
* convert url/urlname to link element (GPX 1.0 -> GPX 1.1).
* @param attr attributes
*/
private void convertUrlToLink(Map attr) {
String url = (String) attr.get("url");
String urlname = (String) attr.get("urlname");
if (url != null) {
if (!attr.containsKey(META_LINKS)) {
attr.put(META_LINKS, new LinkedList());
}
GpxLink link = new GpxLink(url);
link.text = urlname;
@SuppressWarnings("unchecked")
Collection links = (Collection) attr.get(META_LINKS);
links.add(link);
}
}
void tryToFinish() throws SAXException {
List remainingElements = new ArrayList<>(elements);
for (int i = remainingElements.size() - 1; i >= 0; i--) {
endElement(null, remainingElements.get(i), remainingElements.get(i));
}
endDocument();
}
}
/**
* Constructs a new {@code GpxReader}, which can later parse the input stream
* and store the result in trackData and markerData
*
* @param source the source input stream
* @throws IOException if an IO error occurs, e.g. the input stream is closed.
*/
public GpxReader(InputStream source) throws IOException {
Reader utf8stream = UTFInputStreamReader.create(source);
Reader filtered = new InvalidXmlCharacterFilter(utf8stream);
this.inputSource = new InputSource(filtered);
}
/**
* Parse the GPX data.
*
* @param tryToFinish true, if the reader should return at least part of the GPX
* data in case of an error.
* @return true if file was properly parsed, false if there was error during
* parsing but some data were parsed anyway
* @throws SAXException if any SAX parsing error occurs
* @throws IOException if any I/O error occurs
*/
public boolean parse(boolean tryToFinish) throws SAXException, IOException {
Parser parser = new Parser();
try {
Utils.parseSafeSAX(inputSource, parser);
return true;
} catch (SAXException e) {
if (tryToFinish) {
parser.tryToFinish();
if (parser.data.isEmpty())
throw e;
String message = e.getMessage();
if (e instanceof SAXParseException) {
SAXParseException spe = (SAXParseException) e;
message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber());
}
Main.warn(message);
return false;
} else
throw e;
} catch (ParserConfigurationException e) {
Main.error(e); // broken SAXException chaining
throw new SAXException(e);
}
}
/**
* Replies the GPX data.
* @return The GPX data
*/
public GpxData getGpxData() {
return gpxData;
}
}