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.Extensions; 022import org.openstreetmap.josm.data.gpx.GpxConstants; 023import org.openstreetmap.josm.data.gpx.GpxData; 024import org.openstreetmap.josm.data.gpx.GpxLink; 025import org.openstreetmap.josm.data.gpx.GpxRoute; 026import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack; 027import org.openstreetmap.josm.data.gpx.WayPoint; 028import org.openstreetmap.josm.tools.Logging; 029import org.openstreetmap.josm.tools.XmlUtils; 030import org.xml.sax.Attributes; 031import org.xml.sax.InputSource; 032import org.xml.sax.SAXException; 033import org.xml.sax.SAXParseException; 034import org.xml.sax.helpers.DefaultHandler; 035 036/** 037 * Read a gpx file. 038 * 039 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br> 040 * Both GPX version 1.0 and 1.1 are supported. 041 * 042 * @author imi, ramack 043 */ 044public class GpxReader implements GpxConstants, IGpxReader { 045 046 private enum State { 047 INIT, 048 GPX, 049 METADATA, 050 WPT, 051 RTE, 052 TRK, 053 EXT, 054 AUTHOR, 055 LINK, 056 TRKSEG, 057 COPYRIGHT 058 } 059 060 private String version; 061 /** The resulting gpx data */ 062 private GpxData gpxData; 063 private final InputSource inputSource; 064 065 private class Parser extends DefaultHandler { 066 067 private GpxData data; 068 private Collection<Collection<WayPoint>> currentTrack; 069 private Map<String, Object> currentTrackAttr; 070 private Collection<WayPoint> currentTrackSeg; 071 private GpxRoute currentRoute; 072 private WayPoint currentWayPoint; 073 074 private State currentState = State.INIT; 075 076 private GpxLink currentLink; 077 private Extensions currentExtensions; 078 private Stack<State> states; 079 private final Stack<String> elements = new Stack<>(); 080 081 private StringBuilder accumulator = new StringBuilder(); 082 083 private boolean nokiaSportsTrackerBug; 084 085 @Override 086 public void startDocument() { 087 accumulator = new StringBuilder(); 088 states = new Stack<>(); 089 data = new GpxData(); 090 } 091 092 private double parseCoord(Attributes atts, String key) { 093 String val = atts.getValue(key); 094 if (val != null) { 095 return parseCoord(val); 096 } else { 097 // Some software do not respect GPX schema and use "minLat" / "minLon" instead of "minlat" / "minlon" 098 return parseCoord(atts.getValue(key.replaceFirst("l", "L"))); 099 } 100 } 101 102 private double parseCoord(String s) { 103 if (s != null) { 104 try { 105 return Double.parseDouble(s); 106 } catch (NumberFormatException ex) { 107 Logging.trace(ex); 108 } 109 } 110 return Double.NaN; 111 } 112 113 private LatLon parseLatLon(Attributes atts) { 114 return new LatLon( 115 parseCoord(atts, "lat"), 116 parseCoord(atts, "lon")); 117 } 118 119 @Override 120 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 121 elements.push(localName); 122 switch(currentState) { 123 case INIT: 124 states.push(currentState); 125 currentState = State.GPX; 126 data.creator = atts.getValue("creator"); 127 version = atts.getValue("version"); 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 132 version = "1.1"; 133 } 134 break; 135 case GPX: 136 switch (localName) { 137 case "metadata": 138 states.push(currentState); 139 currentState = State.METADATA; 140 break; 141 case "wpt": 142 states.push(currentState); 143 currentState = State.WPT; 144 currentWayPoint = new WayPoint(parseLatLon(atts)); 145 break; 146 case "rte": 147 states.push(currentState); 148 currentState = State.RTE; 149 currentRoute = new GpxRoute(); 150 break; 151 case "trk": 152 states.push(currentState); 153 currentState = State.TRK; 154 currentTrack = new ArrayList<>(); 155 currentTrackAttr = new HashMap<>(); 156 break; 157 case "extensions": 158 states.push(currentState); 159 currentState = State.EXT; 160 currentExtensions = new Extensions(); 161 break; 162 case "gpx": 163 if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) { 164 nokiaSportsTrackerBug = true; 165 } 166 break; 167 default: // Do nothing 168 } 169 break; 170 case METADATA: 171 switch (localName) { 172 case "author": 173 states.push(currentState); 174 currentState = State.AUTHOR; 175 break; 176 case "extensions": 177 states.push(currentState); 178 currentState = State.EXT; 179 currentExtensions = new Extensions(); 180 break; 181 case "copyright": 182 states.push(currentState); 183 currentState = State.COPYRIGHT; 184 data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author")); 185 break; 186 case "link": 187 states.push(currentState); 188 currentState = State.LINK; 189 currentLink = new GpxLink(atts.getValue("href")); 190 break; 191 case "bounds": 192 data.put(META_BOUNDS, new Bounds( 193 parseCoord(atts, "minlat"), 194 parseCoord(atts, "minlon"), 195 parseCoord(atts, "maxlat"), 196 parseCoord(atts, "maxlon"))); 197 break; 198 default: // Do nothing 199 } 200 break; 201 case AUTHOR: 202 switch (localName) { 203 case "link": 204 states.push(currentState); 205 currentState = State.LINK; 206 currentLink = new GpxLink(atts.getValue("href")); 207 break; 208 case "email": 209 data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain")); 210 break; 211 default: // Do nothing 212 } 213 break; 214 case TRK: 215 switch (localName) { 216 case "trkseg": 217 states.push(currentState); 218 currentState = State.TRKSEG; 219 currentTrackSeg = new ArrayList<>(); 220 break; 221 case "link": 222 states.push(currentState); 223 currentState = State.LINK; 224 currentLink = new GpxLink(atts.getValue("href")); 225 break; 226 case "extensions": 227 states.push(currentState); 228 currentState = State.EXT; 229 currentExtensions = new Extensions(); 230 break; 231 default: // Do nothing 232 } 233 break; 234 case TRKSEG: 235 if ("trkpt".equals(localName)) { 236 states.push(currentState); 237 currentState = State.WPT; 238 currentWayPoint = new WayPoint(parseLatLon(atts)); 239 } 240 break; 241 case WPT: 242 switch (localName) { 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 currentExtensions = new Extensions(); 252 break; 253 default: // Do nothing 254 } 255 break; 256 case RTE: 257 switch (localName) { 258 case "link": 259 states.push(currentState); 260 currentState = State.LINK; 261 currentLink = new GpxLink(atts.getValue("href")); 262 break; 263 case "rtept": 264 states.push(currentState); 265 currentState = State.WPT; 266 currentWayPoint = new WayPoint(parseLatLon(atts)); 267 break; 268 case "extensions": 269 states.push(currentState); 270 currentState = State.EXT; 271 currentExtensions = new Extensions(); 272 break; 273 default: // Do nothing 274 } 275 break; 276 default: // Do nothing 277 } 278 accumulator.setLength(0); 279 } 280 281 @Override 282 public void characters(char[] ch, int start, int length) { 283 /** 284 * Remove illegal characters generated by the Nokia Sports Tracker device. 285 * Don't do this crude substitution for all files, since it would destroy 286 * certain unicode characters. 287 */ 288 if (nokiaSportsTrackerBug) { 289 for (int i = 0; i < ch.length; ++i) { 290 if (ch[i] == 1) { 291 ch[i] = 32; 292 } 293 } 294 nokiaSportsTrackerBug = false; 295 } 296 297 accumulator.append(ch, start, length); 298 } 299 300 private Map<String, Object> getAttr() { 301 switch (currentState) { 302 case RTE: return currentRoute.attr; 303 case METADATA: return data.attr; 304 case WPT: return currentWayPoint.attr; 305 case TRK: return currentTrackAttr; 306 default: return null; 307 } 308 } 309 310 @SuppressWarnings("unchecked") 311 @Override 312 public void endElement(String namespaceURI, String localName, String qName) { 313 elements.pop(); 314 switch (currentState) { 315 case GPX: // GPX 1.0 316 case METADATA: // GPX 1.1 317 switch (localName) { 318 case "name": 319 data.put(META_NAME, accumulator.toString()); 320 break; 321 case "desc": 322 data.put(META_DESC, accumulator.toString()); 323 break; 324 case "time": 325 data.put(META_TIME, accumulator.toString()); 326 break; 327 case "keywords": 328 data.put(META_KEYWORDS, accumulator.toString()); 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 333 data.put(META_AUTHOR_NAME, accumulator.toString()); 334 } 335 break; 336 case "email": 337 if ("1.0".equals(version)) { 338 data.put(META_AUTHOR_EMAIL, accumulator.toString()); 339 } 340 break; 341 case "url": 342 case "urlname": 343 data.put(localName, accumulator.toString()); 344 break; 345 case "metadata": 346 case "gpx": 347 if ((currentState == State.METADATA && "metadata".equals(localName)) || 348 (currentState == State.GPX && "gpx".equals(localName))) { 349 convertUrlToLink(data.attr); 350 if (currentExtensions != null && !currentExtensions.isEmpty()) { 351 data.put(META_EXTENSIONS, currentExtensions); 352 } 353 currentState = states.pop(); 354 } 355 break; 356 case "bounds": 357 // do nothing, has been parsed on startElement 358 break; 359 default: 360 //TODO: parse extensions 361 } 362 break; 363 case AUTHOR: 364 switch (localName) { 365 case "author": 366 currentState = states.pop(); 367 break; 368 case "name": 369 data.put(META_AUTHOR_NAME, accumulator.toString()); 370 break; 371 case "email": 372 // do nothing, has been parsed on startElement 373 break; 374 case "link": 375 data.put(META_AUTHOR_LINK, currentLink); 376 break; 377 default: // Do nothing 378 } 379 break; 380 case COPYRIGHT: 381 switch (localName) { 382 case "copyright": 383 currentState = states.pop(); 384 break; 385 case "year": 386 data.put(META_COPYRIGHT_YEAR, accumulator.toString()); 387 break; 388 case "license": 389 data.put(META_COPYRIGHT_LICENSE, accumulator.toString()); 390 break; 391 default: // Do nothing 392 } 393 break; 394 case LINK: 395 switch (localName) { 396 case "text": 397 currentLink.text = accumulator.toString(); 398 break; 399 case "type": 400 currentLink.type = accumulator.toString(); 401 break; 402 case "link": 403 if (currentLink.uri == null && accumulator != null && !accumulator.toString().isEmpty()) { 404 currentLink = new GpxLink(accumulator.toString()); 405 } 406 currentState = states.pop(); 407 break; 408 default: // Do nothing 409 } 410 if (currentState == State.AUTHOR) { 411 data.put(META_AUTHOR_LINK, currentLink); 412 } else if (currentState != State.LINK) { 413 Map<String, Object> attr = getAttr(); 414 if (attr != null && !attr.containsKey(META_LINKS)) { 415 attr.put(META_LINKS, new LinkedList<GpxLink>()); 416 } 417 if (attr != null) 418 ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink); 419 } 420 break; 421 case WPT: 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": 432 currentWayPoint.put(localName, accumulator.toString()); 433 break; 434 case "hdop": 435 case "vdop": 436 case "pdop": 437 try { 438 currentWayPoint.put(localName, Float.valueOf(accumulator.toString())); 439 } catch (NumberFormatException e) { 440 currentWayPoint.put(localName, 0f); 441 } 442 break; 443 case "time": 444 case "cmt": 445 case "desc": 446 currentWayPoint.put(localName, accumulator.toString()); 447 currentWayPoint.setTime(); 448 break; 449 case "rtept": 450 currentState = states.pop(); 451 convertUrlToLink(currentWayPoint.attr); 452 currentRoute.routePoints.add(currentWayPoint); 453 break; 454 case "trkpt": 455 currentState = states.pop(); 456 convertUrlToLink(currentWayPoint.attr); 457 currentTrackSeg.add(currentWayPoint); 458 break; 459 case "wpt": 460 currentState = states.pop(); 461 convertUrlToLink(currentWayPoint.attr); 462 if (currentExtensions != null && !currentExtensions.isEmpty()) { 463 currentWayPoint.put(META_EXTENSIONS, currentExtensions); 464 } 465 data.waypoints.add(currentWayPoint); 466 break; 467 default: // Do nothing 468 } 469 break; 470 case TRKSEG: 471 if ("trkseg".equals(localName)) { 472 currentState = states.pop(); 473 currentTrack.add(currentTrackSeg); 474 } 475 break; 476 case TRK: 477 switch (localName) { 478 case "trk": 479 currentState = states.pop(); 480 convertUrlToLink(currentTrackAttr); 481 data.addTrack(new ImmutableGpxTrack(currentTrack, currentTrackAttr)); 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": 491 currentTrackAttr.put(localName, accumulator.toString()); 492 break; 493 default: // Do nothing 494 } 495 break; 496 case EXT: 497 if ("extensions".equals(localName)) { 498 currentState = states.pop(); 499 } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) { 500 // only interested in extensions written by JOSM 501 currentExtensions.put(localName, accumulator.toString()); 502 } 503 break; 504 default: 505 switch (localName) { 506 case "wpt": 507 currentState = states.pop(); 508 break; 509 case "rte": 510 currentState = states.pop(); 511 convertUrlToLink(currentRoute.attr); 512 data.addRoute(currentRoute); 513 break; 514 default: // Do nothing 515 } 516 } 517 } 518 519 @Override 520 public void endDocument() throws SAXException { 521 if (!states.empty()) 522 throw new SAXException(tr("Parse error: invalid document structure for GPX document.")); 523 Extensions metaExt = (Extensions) data.get(META_EXTENSIONS); 524 if (metaExt != null && "true".equals(metaExt.get("from-server"))) { 525 data.fromServer = true; 526 } 527 gpxData = data; 528 } 529 530 /** 531 * convert url/urlname to link element (GPX 1.0 -> GPX 1.1). 532 * @param attr attributes 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) { 538 if (!attr.containsKey(META_LINKS)) { 539 attr.put(META_LINKS, new LinkedList<GpxLink>()); 540 } 541 GpxLink link = new GpxLink(url); 542 link.text = urlname; 543 @SuppressWarnings("unchecked") 544 Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS); 545 links.add(link); 546 } 547 } 548 549 void tryToFinish() throws SAXException { 550 List<String> remainingElements = new ArrayList<>(elements); 551 for (int i = remainingElements.size() - 1; i >= 0; i--) { 552 endElement(null, remainingElements.get(i), remainingElements.get(i)); 553 } 554 endDocument(); 555 } 556 } 557 558 /** 559 * Constructs a new {@code GpxReader}, which can later parse the input stream 560 * and store the result in trackData and markerData 561 * 562 * @param source the source input stream 563 * @throws IOException if an IO error occurs, e.g. the input stream is closed. 564 */ 565 public GpxReader(InputStream source) throws IOException { 566 Reader utf8stream = UTFInputStreamReader.create(source); 567 Reader filtered = new InvalidXmlCharacterFilter(utf8stream); 568 this.inputSource = new InputSource(filtered); 569 } 570 571 /** 572 * Parse the GPX data. 573 * 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 578 * @throws SAXException if any SAX parsing error occurs 579 * @throws IOException if any I/O error occurs 580 */ 581 @Override 582 public boolean parse(boolean tryToFinish) throws SAXException, IOException { 583 Parser parser = new Parser(); 584 try { 585 XmlUtils.parseSafeSAX(inputSource, parser); 586 return true; 587 } catch (SAXException e) { 588 if (tryToFinish) { 589 parser.tryToFinish(); 590 if (parser.data.isEmpty()) 591 throw e; 592 String message = e.getMessage(); 593 if (e instanceof SAXParseException) { 594 SAXParseException spe = (SAXParseException) e; 595 message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber()); 596 } 597 Logging.warn(message); 598 return false; 599 } else 600 throw e; 601 } catch (ParserConfigurationException e) { 602 Logging.error(e); // broken SAXException chaining 603 throw new SAXException(e); 604 } 605 } 606 607 @Override 608 public GpxData getGpxData() { 609 return gpxData; 610 } 611}