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

Last change on this file since 17846 was 17846, checked in by simon04, 3 years ago

fix #20793 - Reduce memory consumption for GpxExtensionCollection (patch by Bjoeni, modified)

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