001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.IOException; 007import java.io.InputStream; 008import java.io.Reader; 009import java.util.ArrayList; 010import java.util.Collection; 011import java.util.HashMap; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Map; 015import java.util.Stack; 016 017import javax.xml.parsers.ParserConfigurationException; 018 019import org.openstreetmap.josm.data.Bounds; 020import org.openstreetmap.josm.data.coor.LatLon; 021import org.openstreetmap.josm.data.gpx.GpxConstants; 022import org.openstreetmap.josm.data.gpx.GpxData; 023import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace; 024import org.openstreetmap.josm.data.gpx.GpxExtensionCollection; 025import org.openstreetmap.josm.data.gpx.GpxLink; 026import org.openstreetmap.josm.data.gpx.GpxRoute; 027import org.openstreetmap.josm.data.gpx.GpxTrack; 028import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 029import org.openstreetmap.josm.data.gpx.IGpxTrackSegment; 030import org.openstreetmap.josm.data.gpx.WayPoint; 031import org.openstreetmap.josm.tools.Logging; 032import org.openstreetmap.josm.tools.UncheckedParseException; 033import org.openstreetmap.josm.tools.XmlUtils; 034import org.openstreetmap.josm.tools.date.DateUtils; 035import org.xml.sax.Attributes; 036import org.xml.sax.InputSource; 037import org.xml.sax.SAXException; 038import org.xml.sax.SAXParseException; 039import org.xml.sax.helpers.DefaultHandler; 040 041/** 042 * Read a gpx file. 043 * 044 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br> 045 * Both GPX version 1.0 and 1.1 are supported. 046 * 047 * @author imi, ramack 048 */ 049public class GpxReader implements GpxConstants, IGpxReader { 050 051 private enum State { 052 INIT, 053 GPX, 054 METADATA, 055 WPT, 056 RTE, 057 TRK, 058 EXT, 059 AUTHOR, 060 LINK, 061 TRKSEG, 062 COPYRIGHT 063 } 064 065 private String version; 066 /** The resulting gpx data */ 067 private GpxData gpxData; 068 private final InputSource inputSource; 069 070 private class Parser extends DefaultHandler { 071 072 private GpxData data; 073 private Collection<IGpxTrackSegment> currentTrack; 074 private Map<String, Object> currentTrackAttr; 075 private Collection<WayPoint> currentTrackSeg; 076 private GpxRoute currentRoute; 077 private WayPoint currentWayPoint; 078 079 private State currentState = State.INIT; 080 081 private GpxLink currentLink; 082 private GpxExtensionCollection currentExtensionCollection; 083 private GpxExtensionCollection currentTrackExtensionCollection; 084 private Stack<State> states; 085 private final Stack<String> elements = new Stack<>(); 086 087 private StringBuilder accumulator = new StringBuilder(); 088 089 private boolean nokiaSportsTrackerBug; 090 091 @Override 092 public void startDocument() { 093 accumulator = new StringBuilder(); 094 states = new Stack<>(); 095 data = new GpxData(true); 096 currentExtensionCollection = new GpxExtensionCollection(); 097 currentTrackExtensionCollection = new GpxExtensionCollection(); 098 } 099 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 != 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 -> 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); 624 Reader filtered = new InvalidXmlCharacterFilter(utf8stream); 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 if (parser.data.isEmpty()) 648 throw e; 649 String message = e.getMessage(); 650 if (e instanceof SAXParseException) { 651 SAXParseException spe = (SAXParseException) e; 652 message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber()); 653 } 654 Logging.warn(message); 655 return false; 656 } else 657 throw e; 658 } catch (ParserConfigurationException e) { 659 Logging.error(e); // broken SAXException chaining 660 throw new SAXException(e); 661 } 662 } 663 664 @Override 665 public GpxData getGpxData() { 666 return gpxData; 667 } 668}