source: josm/trunk/src/org/openstreetmap/josm/io/GpxReader.java@ 13901

Last change on this file since 13901 was 13901, checked in by Don-vip, 6 years ago

add new XmlUtils class with more "safe factories" methods

  • Property svn:eol-style set to native
File size: 22.6 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[444]2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.io.InputStream;
[6080]8import java.io.Reader;
[582]9import java.util.ArrayList;
[444]10import java.util.Collection;
[2907]11import java.util.HashMap;
[444]12import java.util.LinkedList;
[2795]13import java.util.List;
[582]14import java.util.Map;
[8856]15import java.util.Stack;
[444]16
17import javax.xml.parsers.ParserConfigurationException;
18
[7575]19import org.openstreetmap.josm.data.Bounds;
[444]20import org.openstreetmap.josm.data.coor.LatLon;
[5679]21import org.openstreetmap.josm.data.gpx.Extensions;
[5681]22import org.openstreetmap.josm.data.gpx.GpxConstants;
[444]23import org.openstreetmap.josm.data.gpx.GpxData;
24import org.openstreetmap.josm.data.gpx.GpxLink;
[582]25import org.openstreetmap.josm.data.gpx.GpxRoute;
[2907]26import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
[444]27import org.openstreetmap.josm.data.gpx.WayPoint;
[12620]28import org.openstreetmap.josm.tools.Logging;
[13901]29import org.openstreetmap.josm.tools.XmlUtils;
[444]30import org.xml.sax.Attributes;
31import org.xml.sax.InputSource;
32import org.xml.sax.SAXException;
[6117]33import org.xml.sax.SAXParseException;
[444]34import org.xml.sax.helpers.DefaultHandler;
35
36/**
[5398]37 * Read a gpx file.
[6070]38 *
[7575]39 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br>
[5398]40 * Both GPX version 1.0 and 1.1 are supported.
41 *
[444]42 * @author imi, ramack
43 */
[5681]44public class GpxReader implements GpxConstants {
[444]45
[9059]46 private enum State {
[10216]47 INIT,
48 GPX,
49 METADATA,
50 WPT,
51 RTE,
52 TRK,
53 EXT,
54 AUTHOR,
55 LINK,
56 TRKSEG,
57 COPYRIGHT
[9059]58 }
[8510]59
[5395]60 private String version;
[8510]61 /** The resulting gpx data */
[5679]62 private GpxData gpxData;
[9078]63 private final InputSource inputSource;
[444]64
[1169]65 private class Parser extends DefaultHandler {
[444]66
[5679]67 private GpxData data;
[2907]68 private Collection<Collection<WayPoint>> currentTrack;
69 private Map<String, Object> currentTrackAttr;
[1169]70 private Collection<WayPoint> currentTrackSeg;
71 private GpxRoute currentRoute;
72 private WayPoint currentWayPoint;
[444]73
[10216]74 private State currentState = State.INIT;
[444]75
[1169]76 private GpxLink currentLink;
[5679]77 private Extensions currentExtensions;
[8856]78 private Stack<State> states;
79 private final Stack<String> elements = new Stack<>();
[444]80
[8851]81 private StringBuilder accumulator = new StringBuilder();
[444]82
[8840]83 private boolean nokiaSportsTrackerBug;
[2342]84
[7575]85 @Override
86 public void startDocument() {
[8851]87 accumulator = new StringBuilder();
[8856]88 states = new Stack<>();
[5679]89 data = new GpxData();
[1169]90 }
[512]91
[13194]92 private double parseCoord(Attributes atts, String key) {
93 String val = atts.getValue(key);
94 if (val != null) {
95 return parseCoord(val);
96 } else {
97 // Some software do not respect GPX schema and use "minLat" / "minLon" instead of "minlat" / "minlon"
98 return parseCoord(atts.getValue(key.replaceFirst("l", "L")));
99 }
100 }
101
[1169]102 private double parseCoord(String s) {
[13194]103 if (s != null) {
104 try {
105 return Double.parseDouble(s);
106 } catch (NumberFormatException ex) {
107 Logging.trace(ex);
108 }
[1169]109 }
[13194]110 return Double.NaN;
[1169]111 }
[512]112
[1169]113 private LatLon parseLatLon(Attributes atts) {
114 return new LatLon(
[13194]115 parseCoord(atts, "lat"),
116 parseCoord(atts, "lon"));
[1169]117 }
[444]118
[7012]119 @Override
120 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
[5679]121 elements.push(localName);
[1169]122 switch(currentState) {
[10216]123 case INIT:
[5395]124 states.push(currentState);
[10216]125 currentState = State.GPX;
[5679]126 data.creator = atts.getValue("creator");
[5395]127 version = atts.getValue("version");
[5397]128 if (version != null && version.startsWith("1.0")) {
129 version = "1.0";
130 } else if (!"1.1".equals(version)) {
131 // unknown version, assume 1.1
[5395]132 version = "1.1";
133 }
[5397]134 break;
[10216]135 case GPX:
[7012]136 switch (localName) {
137 case "metadata":
[2789]138 states.push(currentState);
[10216]139 currentState = State.METADATA;
[7012]140 break;
141 case "wpt":
[2789]142 states.push(currentState);
[10216]143 currentState = State.WPT;
[2789]144 currentWayPoint = new WayPoint(parseLatLon(atts));
[7012]145 break;
146 case "rte":
[2789]147 states.push(currentState);
[10216]148 currentState = State.RTE;
[2789]149 currentRoute = new GpxRoute();
[7012]150 break;
151 case "trk":
[2789]152 states.push(currentState);
[10216]153 currentState = State.TRK;
[7005]154 currentTrack = new ArrayList<>();
155 currentTrackAttr = new HashMap<>();
[7012]156 break;
157 case "extensions":
[2789]158 states.push(currentState);
[10216]159 currentState = State.EXT;
[5679]160 currentExtensions = new Extensions();
[7012]161 break;
162 case "gpx":
163 if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) {
164 nokiaSportsTrackerBug = true;
165 }
[10216]166 break;
167 default: // Do nothing
[2789]168 }
169 break;
[10216]170 case METADATA:
[7012]171 switch (localName) {
172 case "author":
[5395]173 states.push(currentState);
[10216]174 currentState = State.AUTHOR;
[7012]175 break;
176 case "extensions":
[5395]177 states.push(currentState);
[10216]178 currentState = State.EXT;
[5679]179 currentExtensions = new Extensions();
[7012]180 break;
181 case "copyright":
[5395]182 states.push(currentState);
[10216]183 currentState = State.COPYRIGHT;
[7518]184 data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author"));
[7012]185 break;
186 case "link":
[5395]187 states.push(currentState);
[10216]188 currentState = State.LINK;
[5395]189 currentLink = new GpxLink(atts.getValue("href"));
[7575]190 break;
191 case "bounds":
192 data.put(META_BOUNDS, new Bounds(
[13194]193 parseCoord(atts, "minlat"),
194 parseCoord(atts, "minlon"),
195 parseCoord(atts, "maxlat"),
196 parseCoord(atts, "maxlon")));
[10216]197 break;
198 default: // Do nothing
[5395]199 }
200 break;
[10216]201 case AUTHOR:
[7012]202 switch (localName) {
203 case "link":
[2789]204 states.push(currentState);
[10216]205 currentState = State.LINK;
[2789]206 currentLink = new GpxLink(atts.getValue("href"));
[7012]207 break;
208 case "email":
[8846]209 data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain"));
[10216]210 break;
211 default: // Do nothing
[2789]212 }
213 break;
[10216]214 case TRK:
[7012]215 switch (localName) {
216 case "trkseg":
[2789]217 states.push(currentState);
[10216]218 currentState = State.TRKSEG;
[7005]219 currentTrackSeg = new ArrayList<>();
[7012]220 break;
221 case "link":
[2789]222 states.push(currentState);
[10216]223 currentState = State.LINK;
[2789]224 currentLink = new GpxLink(atts.getValue("href"));
[7012]225 break;
226 case "extensions":
[2789]227 states.push(currentState);
[10216]228 currentState = State.EXT;
[5679]229 currentExtensions = new Extensions();
[10216]230 break;
231 default: // Do nothing
[2789]232 }
233 break;
[10216]234 case TRKSEG:
[7012]235 if ("trkpt".equals(localName)) {
[2789]236 states.push(currentState);
[10216]237 currentState = State.WPT;
[2789]238 currentWayPoint = new WayPoint(parseLatLon(atts));
239 }
240 break;
[10216]241 case WPT:
[7012]242 switch (localName) {
243 case "link":
[2789]244 states.push(currentState);
[10216]245 currentState = State.LINK;
[2789]246 currentLink = new GpxLink(atts.getValue("href"));
[7012]247 break;
248 case "extensions":
[2789]249 states.push(currentState);
[10216]250 currentState = State.EXT;
[5679]251 currentExtensions = new Extensions();
[7012]252 break;
[10216]253 default: // Do nothing
[2789]254 }
255 break;
[10216]256 case RTE:
[7012]257 switch (localName) {
258 case "link":
[2789]259 states.push(currentState);
[10216]260 currentState = State.LINK;
[2789]261 currentLink = new GpxLink(atts.getValue("href"));
[7012]262 break;
263 case "rtept":
[2789]264 states.push(currentState);
[10216]265 currentState = State.WPT;
[2789]266 currentWayPoint = new WayPoint(parseLatLon(atts));
[7012]267 break;
268 case "extensions":
[2789]269 states.push(currentState);
[10216]270 currentState = State.EXT;
[5679]271 currentExtensions = new Extensions();
[7012]272 break;
[10216]273 default: // Do nothing
[2789]274 }
275 break;
[10216]276 default: // Do nothing
[1169]277 }
278 accumulator.setLength(0);
279 }
[444]280
[7012]281 @Override
282 public void characters(char[] ch, int start, int length) {
[2512]283 /**
[2342]284 * Remove illegal characters generated by the Nokia Sports Tracker device.
[2512]285 * Don't do this crude substitution for all files, since it would destroy
[2342]286 * certain unicode characters.
287 */
288 if (nokiaSportsTrackerBug) {
[8510]289 for (int i = 0; i < ch.length; ++i) {
[2342]290 if (ch[i] == 1) {
291 ch[i] = 32;
292 }
293 }
294 nokiaSportsTrackerBug = false;
295 }
296
[1169]297 accumulator.append(ch, start, length);
298 }
[444]299
[1169]300 private Map<String, Object> getAttr() {
301 switch (currentState) {
[10216]302 case RTE: return currentRoute.attr;
303 case METADATA: return data.attr;
304 case WPT: return currentWayPoint.attr;
305 case TRK: return currentTrackAttr;
[2789]306 default: return null;
[1169]307 }
308 }
[512]309
[2789]310 @SuppressWarnings("unchecked")
[7012]311 @Override
312 public void endElement(String namespaceURI, String localName, String qName) {
[2795]313 elements.pop();
[1169]314 switch (currentState) {
[10216]315 case GPX: // GPX 1.0
316 case METADATA: // GPX 1.1
[7012]317 switch (localName) {
318 case "name":
[7518]319 data.put(META_NAME, accumulator.toString());
[7012]320 break;
321 case "desc":
[7518]322 data.put(META_DESC, accumulator.toString());
[7012]323 break;
324 case "time":
[7518]325 data.put(META_TIME, accumulator.toString());
[7012]326 break;
327 case "keywords":
[7518]328 data.put(META_KEYWORDS, accumulator.toString());
[7012]329 break;
330 case "author":
331 if ("1.0".equals(version)) {
332 // author is a string in 1.0, but complex element in 1.1
[7518]333 data.put(META_AUTHOR_NAME, accumulator.toString());
[7012]334 }
335 break;
336 case "email":
337 if ("1.0".equals(version)) {
[7518]338 data.put(META_AUTHOR_EMAIL, accumulator.toString());
[7012]339 }
340 break;
341 case "url":
342 case "urlname":
[7518]343 data.put(localName, accumulator.toString());
[7012]344 break;
345 case "metadata":
346 case "gpx":
[10216]347 if ((currentState == State.METADATA && "metadata".equals(localName)) ||
348 (currentState == State.GPX && "gpx".equals(localName))) {
[7012]349 convertUrlToLink(data.attr);
350 if (currentExtensions != null && !currentExtensions.isEmpty()) {
[7518]351 data.put(META_EXTENSIONS, currentExtensions);
[7012]352 }
353 currentState = states.pop();
[5679]354 }
[11675]355 break;
[7575]356 case "bounds":
357 // do nothing, has been parsed on startElement
358 break;
[7012]359 default:
[7575]360 //TODO: parse extensions
[2789]361 }
362 break;
[10216]363 case AUTHOR:
[7012]364 switch (localName) {
365 case "author":
[2789]366 currentState = states.pop();
[7012]367 break;
368 case "name":
[7518]369 data.put(META_AUTHOR_NAME, accumulator.toString());
[7012]370 break;
371 case "email":
[2789]372 // do nothing, has been parsed on startElement
[7012]373 break;
374 case "link":
[7518]375 data.put(META_AUTHOR_LINK, currentLink);
[7012]376 break;
[10216]377 default: // Do nothing
[2789]378 }
379 break;
[10216]380 case COPYRIGHT:
[7012]381 switch (localName) {
382 case "copyright":
[2789]383 currentState = states.pop();
[7012]384 break;
385 case "year":
[7518]386 data.put(META_COPYRIGHT_YEAR, accumulator.toString());
[7012]387 break;
388 case "license":
[7518]389 data.put(META_COPYRIGHT_LICENSE, accumulator.toString());
[7012]390 break;
[10216]391 default: // Do nothing
[2789]392 }
393 break;
[10216]394 case LINK:
[7012]395 switch (localName) {
396 case "text":
[2789]397 currentLink.text = accumulator.toString();
[7012]398 break;
399 case "type":
[2789]400 currentLink.type = accumulator.toString();
[7012]401 break;
402 case "link":
[8461]403 if (currentLink.uri == null && accumulator != null && !accumulator.toString().isEmpty()) {
[2789]404 currentLink = new GpxLink(accumulator.toString());
[1169]405 }
[2789]406 currentState = states.pop();
[7012]407 break;
[10216]408 default: // Do nothing
[2789]409 }
[10216]410 if (currentState == State.AUTHOR) {
[7518]411 data.put(META_AUTHOR_LINK, currentLink);
[10216]412 } else if (currentState != State.LINK) {
[2789]413 Map<String, Object> attr = getAttr();
[11397]414 if (attr != null && !attr.containsKey(META_LINKS)) {
[5681]415 attr.put(META_LINKS, new LinkedList<GpxLink>());
[2211]416 }
[11397]417 if (attr != null)
418 ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink);
[2789]419 }
420 break;
[10216]421 case WPT:
[7012]422 switch (localName) {
423 case "ele":
424 case "magvar":
425 case "name":
426 case "src":
427 case "geoidheight":
428 case "type":
429 case "sym":
430 case "url":
431 case "urlname":
[7518]432 currentWayPoint.put(localName, accumulator.toString());
[7012]433 break;
434 case "hdop":
435 case "vdop":
436 case "pdop":
[2789]437 try {
[8390]438 currentWayPoint.put(localName, Float.valueOf(accumulator.toString()));
[10212]439 } catch (NumberFormatException e) {
[10045]440 currentWayPoint.put(localName, 0f);
[1425]441 }
[7012]442 break;
443 case "time":
444 case "cmt":
445 case "desc":
[7518]446 currentWayPoint.put(localName, accumulator.toString());
[2789]447 currentWayPoint.setTime();
[7012]448 break;
449 case "rtept":
[2789]450 currentState = states.pop();
[5398]451 convertUrlToLink(currentWayPoint.attr);
[2789]452 currentRoute.routePoints.add(currentWayPoint);
[7012]453 break;
454 case "trkpt":
[2789]455 currentState = states.pop();
[5398]456 convertUrlToLink(currentWayPoint.attr);
[2789]457 currentTrackSeg.add(currentWayPoint);
[7012]458 break;
459 case "wpt":
[2789]460 currentState = states.pop();
[5398]461 convertUrlToLink(currentWayPoint.attr);
[5684]462 if (currentExtensions != null && !currentExtensions.isEmpty()) {
[7518]463 currentWayPoint.put(META_EXTENSIONS, currentExtensions);
[5684]464 }
[5679]465 data.waypoints.add(currentWayPoint);
[7012]466 break;
[10216]467 default: // Do nothing
[2789]468 }
469 break;
[10216]470 case TRKSEG:
[7012]471 if ("trkseg".equals(localName)) {
[2789]472 currentState = states.pop();
[2907]473 currentTrack.add(currentTrackSeg);
[2789]474 }
475 break;
[10216]476 case TRK:
[7012]477 switch (localName) {
478 case "trk":
[2789]479 currentState = states.pop();
[5398]480 convertUrlToLink(currentTrackAttr);
[12156]481 data.addTrack(new ImmutableGpxTrack(currentTrack, currentTrackAttr));
[7012]482 break;
483 case "name":
484 case "cmt":
485 case "desc":
486 case "src":
487 case "type":
488 case "number":
489 case "url":
490 case "urlname":
[5679]491 currentTrackAttr.put(localName, accumulator.toString());
[7012]492 break;
[10216]493 default: // Do nothing
[2789]494 }
495 break;
[10216]496 case EXT:
[7012]497 if ("extensions".equals(localName)) {
[2789]498 currentState = states.pop();
[5681]499 } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) {
[7012]500 // only interested in extensions written by JOSM
[5679]501 currentExtensions.put(localName, accumulator.toString());
[2789]502 }
503 break;
504 default:
[7012]505 switch (localName) {
506 case "wpt":
[2789]507 currentState = states.pop();
[7012]508 break;
509 case "rte":
[2789]510 currentState = states.pop();
[5398]511 convertUrlToLink(currentRoute.attr);
[12156]512 data.addRoute(currentRoute);
[7012]513 break;
[10216]514 default: // Do nothing
[2789]515 }
[1169]516 }
517 }
[444]518
[7012]519 @Override
[10378]520 public void endDocument() throws SAXException {
[8856]521 if (!states.empty())
[2181]522 throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
[7518]523 Extensions metaExt = (Extensions) data.get(META_EXTENSIONS);
[5679]524 if (metaExt != null && "true".equals(metaExt.get("from-server"))) {
525 data.fromServer = true;
526 }
527 gpxData = data;
[1169]528 }
[2795]529
[5398]530 /**
[6830]531 * convert url/urlname to link element (GPX 1.0 -&gt; GPX 1.1).
[9231]532 * @param attr attributes
[5398]533 */
534 private void convertUrlToLink(Map<String, Object> attr) {
535 String url = (String) attr.get("url");
536 String urlname = (String) attr.get("urlname");
537 if (url != null) {
[5681]538 if (!attr.containsKey(META_LINKS)) {
539 attr.put(META_LINKS, new LinkedList<GpxLink>());
[5398]540 }
541 GpxLink link = new GpxLink(url);
542 link.text = urlname;
[9231]543 @SuppressWarnings("unchecked")
[6142]544 Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS);
[5590]545 links.add(link);
[5398]546 }
547 }
548
[10216]549 void tryToFinish() throws SAXException {
[7005]550 List<String> remainingElements = new ArrayList<>(elements);
[8510]551 for (int i = remainingElements.size() - 1; i >= 0; i--) {
[4819]552 endElement(null, remainingElements.get(i), remainingElements.get(i));
[2795]553 }
554 endDocument();
555 }
[1169]556 }
557
558 /**
[7509]559 * Constructs a new {@code GpxReader}, which can later parse the input stream
[7033]560 * and store the result in trackData and markerData
[1169]561 *
[5854]562 * @param source the source input stream
563 * @throws IOException if an IO error occurs, e.g. the input stream is closed.
[1169]564 */
[2795]565 public GpxReader(InputStream source) throws IOException {
[6650]566 Reader utf8stream = UTFInputStreamReader.create(source);
[6080]567 Reader filtered = new InvalidXmlCharacterFilter(utf8stream);
568 this.inputSource = new InputSource(filtered);
[2795]569 }
[1169]570
[2795]571 /**
[5854]572 * Parse the GPX data.
[2801]573 *
[5854]574 * @param tryToFinish true, if the reader should return at least part of the GPX
575 * data in case of an error.
576 * @return true if file was properly parsed, false if there was error during
577 * parsing but some data were parsed anyway
[8470]578 * @throws SAXException if any SAX parsing error occurs
579 * @throws IOException if any I/O error occurs
[2795]580 */
581 public boolean parse(boolean tryToFinish) throws SAXException, IOException {
[1169]582 Parser parser = new Parser();
583 try {
[13901]584 XmlUtils.parseSafeSAX(inputSource, parser);
[2795]585 return true;
586 } catch (SAXException e) {
587 if (tryToFinish) {
588 parser.tryToFinish();
[5679]589 if (parser.data.isEmpty())
[2795]590 throw e;
[6117]591 String message = e.getMessage();
592 if (e instanceof SAXParseException) {
[8510]593 SAXParseException spe = (SAXParseException) e;
[8846]594 message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber());
[6117]595 }
[12620]596 Logging.warn(message);
[2795]597 return false;
598 } else
599 throw e;
[1169]600 } catch (ParserConfigurationException e) {
[12620]601 Logging.error(e); // broken SAXException chaining
[1169]602 throw new SAXException(e);
603 }
604 }
[5679]605
[6117]606 /**
607 * Replies the GPX data.
608 * @return The GPX data
609 */
[5679]610 public GpxData getGpxData() {
611 return gpxData;
612 }
[444]613}
Note: See TracBrowser for help on using the repository browser.