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

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

fix #18852 - avoid useless null-check (patch by hiddewie)

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