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.Main; 020import org.openstreetmap.josm.data.Bounds; 021import org.openstreetmap.josm.data.coor.LatLon; 022import org.openstreetmap.josm.data.gpx.Extensions; 023import org.openstreetmap.josm.data.gpx.GpxConstants; 024import org.openstreetmap.josm.data.gpx.GpxData; 025import org.openstreetmap.josm.data.gpx.GpxLink; 026import org.openstreetmap.josm.data.gpx.GpxRoute; 027import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack; 028import org.openstreetmap.josm.data.gpx.WayPoint; 029import org.openstreetmap.josm.tools.Utils; 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 { 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(String s) { 093 try { 094 return Double.parseDouble(s); 095 } catch (NumberFormatException ex) { 096 return Double.NaN; 097 } 098 } 099 100 private LatLon parseLatLon(Attributes atts) { 101 return new LatLon( 102 parseCoord(atts.getValue("lat")), 103 parseCoord(atts.getValue("lon"))); 104 } 105 106 @Override 107 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 108 elements.push(localName); 109 switch(currentState) { 110 case INIT: 111 states.push(currentState); 112 currentState = State.GPX; 113 data.creator = atts.getValue("creator"); 114 version = atts.getValue("version"); 115 if (version != null && version.startsWith("1.0")) { 116 version = "1.0"; 117 } else if (!"1.1".equals(version)) { 118 // unknown version, assume 1.1 119 version = "1.1"; 120 } 121 break; 122 case GPX: 123 switch (localName) { 124 case "metadata": 125 states.push(currentState); 126 currentState = State.METADATA; 127 break; 128 case "wpt": 129 states.push(currentState); 130 currentState = State.WPT; 131 currentWayPoint = new WayPoint(parseLatLon(atts)); 132 break; 133 case "rte": 134 states.push(currentState); 135 currentState = State.RTE; 136 currentRoute = new GpxRoute(); 137 break; 138 case "trk": 139 states.push(currentState); 140 currentState = State.TRK; 141 currentTrack = new ArrayList<>(); 142 currentTrackAttr = new HashMap<>(); 143 break; 144 case "extensions": 145 states.push(currentState); 146 currentState = State.EXT; 147 currentExtensions = new Extensions(); 148 break; 149 case "gpx": 150 if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) { 151 nokiaSportsTrackerBug = true; 152 } 153 break; 154 default: // Do nothing 155 } 156 break; 157 case METADATA: 158 switch (localName) { 159 case "author": 160 states.push(currentState); 161 currentState = State.AUTHOR; 162 break; 163 case "extensions": 164 states.push(currentState); 165 currentState = State.EXT; 166 currentExtensions = new Extensions(); 167 break; 168 case "copyright": 169 states.push(currentState); 170 currentState = State.COPYRIGHT; 171 data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author")); 172 break; 173 case "link": 174 states.push(currentState); 175 currentState = State.LINK; 176 currentLink = new GpxLink(atts.getValue("href")); 177 break; 178 case "bounds": 179 data.put(META_BOUNDS, new Bounds( 180 parseCoord(atts.getValue("minlat")), 181 parseCoord(atts.getValue("minlon")), 182 parseCoord(atts.getValue("maxlat")), 183 parseCoord(atts.getValue("maxlon")))); 184 break; 185 default: // Do nothing 186 } 187 break; 188 case AUTHOR: 189 switch (localName) { 190 case "link": 191 states.push(currentState); 192 currentState = State.LINK; 193 currentLink = new GpxLink(atts.getValue("href")); 194 break; 195 case "email": 196 data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain")); 197 break; 198 default: // Do nothing 199 } 200 break; 201 case TRK: 202 switch (localName) { 203 case "trkseg": 204 states.push(currentState); 205 currentState = State.TRKSEG; 206 currentTrackSeg = new ArrayList<>(); 207 break; 208 case "link": 209 states.push(currentState); 210 currentState = State.LINK; 211 currentLink = new GpxLink(atts.getValue("href")); 212 break; 213 case "extensions": 214 states.push(currentState); 215 currentState = State.EXT; 216 currentExtensions = new Extensions(); 217 break; 218 default: // Do nothing 219 } 220 break; 221 case TRKSEG: 222 if ("trkpt".equals(localName)) { 223 states.push(currentState); 224 currentState = State.WPT; 225 currentWayPoint = new WayPoint(parseLatLon(atts)); 226 } 227 break; 228 case WPT: 229 switch (localName) { 230 case "link": 231 states.push(currentState); 232 currentState = State.LINK; 233 currentLink = new GpxLink(atts.getValue("href")); 234 break; 235 case "extensions": 236 states.push(currentState); 237 currentState = State.EXT; 238 currentExtensions = new Extensions(); 239 break; 240 default: // Do nothing 241 } 242 break; 243 case RTE: 244 switch (localName) { 245 case "link": 246 states.push(currentState); 247 currentState = State.LINK; 248 currentLink = new GpxLink(atts.getValue("href")); 249 break; 250 case "rtept": 251 states.push(currentState); 252 currentState = State.WPT; 253 currentWayPoint = new WayPoint(parseLatLon(atts)); 254 break; 255 case "extensions": 256 states.push(currentState); 257 currentState = State.EXT; 258 currentExtensions = new Extensions(); 259 break; 260 default: // Do nothing 261 } 262 break; 263 default: // Do nothing 264 } 265 accumulator.setLength(0); 266 } 267 268 @Override 269 public void characters(char[] ch, int start, int length) { 270 /** 271 * Remove illegal characters generated by the Nokia Sports Tracker device. 272 * Don't do this crude substitution for all files, since it would destroy 273 * certain unicode characters. 274 */ 275 if (nokiaSportsTrackerBug) { 276 for (int i = 0; i < ch.length; ++i) { 277 if (ch[i] == 1) { 278 ch[i] = 32; 279 } 280 } 281 nokiaSportsTrackerBug = false; 282 } 283 284 accumulator.append(ch, start, length); 285 } 286 287 private Map<String, Object> getAttr() { 288 switch (currentState) { 289 case RTE: return currentRoute.attr; 290 case METADATA: return data.attr; 291 case WPT: return currentWayPoint.attr; 292 case TRK: return currentTrackAttr; 293 default: return null; 294 } 295 } 296 297 @SuppressWarnings("unchecked") 298 @Override 299 public void endElement(String namespaceURI, String localName, String qName) { 300 elements.pop(); 301 switch (currentState) { 302 case GPX: // GPX 1.0 303 case METADATA: // GPX 1.1 304 switch (localName) { 305 case "name": 306 data.put(META_NAME, accumulator.toString()); 307 break; 308 case "desc": 309 data.put(META_DESC, accumulator.toString()); 310 break; 311 case "time": 312 data.put(META_TIME, accumulator.toString()); 313 break; 314 case "keywords": 315 data.put(META_KEYWORDS, accumulator.toString()); 316 break; 317 case "author": 318 if ("1.0".equals(version)) { 319 // author is a string in 1.0, but complex element in 1.1 320 data.put(META_AUTHOR_NAME, accumulator.toString()); 321 } 322 break; 323 case "email": 324 if ("1.0".equals(version)) { 325 data.put(META_AUTHOR_EMAIL, accumulator.toString()); 326 } 327 break; 328 case "url": 329 case "urlname": 330 data.put(localName, accumulator.toString()); 331 break; 332 case "metadata": 333 case "gpx": 334 if ((currentState == State.METADATA && "metadata".equals(localName)) || 335 (currentState == State.GPX && "gpx".equals(localName))) { 336 convertUrlToLink(data.attr); 337 if (currentExtensions != null && !currentExtensions.isEmpty()) { 338 data.put(META_EXTENSIONS, currentExtensions); 339 } 340 currentState = states.pop(); 341 break; 342 } 343 case "bounds": 344 // do nothing, has been parsed on startElement 345 break; 346 default: 347 //TODO: parse extensions 348 } 349 break; 350 case AUTHOR: 351 switch (localName) { 352 case "author": 353 currentState = states.pop(); 354 break; 355 case "name": 356 data.put(META_AUTHOR_NAME, accumulator.toString()); 357 break; 358 case "email": 359 // do nothing, has been parsed on startElement 360 break; 361 case "link": 362 data.put(META_AUTHOR_LINK, currentLink); 363 break; 364 default: // Do nothing 365 } 366 break; 367 case COPYRIGHT: 368 switch (localName) { 369 case "copyright": 370 currentState = states.pop(); 371 break; 372 case "year": 373 data.put(META_COPYRIGHT_YEAR, accumulator.toString()); 374 break; 375 case "license": 376 data.put(META_COPYRIGHT_LICENSE, accumulator.toString()); 377 break; 378 default: // Do nothing 379 } 380 break; 381 case LINK: 382 switch (localName) { 383 case "text": 384 currentLink.text = accumulator.toString(); 385 break; 386 case "type": 387 currentLink.type = accumulator.toString(); 388 break; 389 case "link": 390 if (currentLink.uri == null && accumulator != null && !accumulator.toString().isEmpty()) { 391 currentLink = new GpxLink(accumulator.toString()); 392 } 393 currentState = states.pop(); 394 break; 395 default: // Do nothing 396 } 397 if (currentState == State.AUTHOR) { 398 data.put(META_AUTHOR_LINK, currentLink); 399 } else if (currentState != State.LINK) { 400 Map<String, Object> attr = getAttr(); 401 if (!attr.containsKey(META_LINKS)) { 402 attr.put(META_LINKS, new LinkedList<GpxLink>()); 403 } 404 ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink); 405 } 406 break; 407 case WPT: 408 switch (localName) { 409 case "ele": 410 case "magvar": 411 case "name": 412 case "src": 413 case "geoidheight": 414 case "type": 415 case "sym": 416 case "url": 417 case "urlname": 418 currentWayPoint.put(localName, accumulator.toString()); 419 break; 420 case "hdop": 421 case "vdop": 422 case "pdop": 423 try { 424 currentWayPoint.put(localName, Float.valueOf(accumulator.toString())); 425 } catch (NumberFormatException e) { 426 currentWayPoint.put(localName, 0f); 427 } 428 break; 429 case "time": 430 case "cmt": 431 case "desc": 432 currentWayPoint.put(localName, accumulator.toString()); 433 currentWayPoint.setTime(); 434 break; 435 case "rtept": 436 currentState = states.pop(); 437 convertUrlToLink(currentWayPoint.attr); 438 currentRoute.routePoints.add(currentWayPoint); 439 break; 440 case "trkpt": 441 currentState = states.pop(); 442 convertUrlToLink(currentWayPoint.attr); 443 currentTrackSeg.add(currentWayPoint); 444 break; 445 case "wpt": 446 currentState = states.pop(); 447 convertUrlToLink(currentWayPoint.attr); 448 if (currentExtensions != null && !currentExtensions.isEmpty()) { 449 currentWayPoint.put(META_EXTENSIONS, currentExtensions); 450 } 451 data.waypoints.add(currentWayPoint); 452 break; 453 default: // Do nothing 454 } 455 break; 456 case TRKSEG: 457 if ("trkseg".equals(localName)) { 458 currentState = states.pop(); 459 currentTrack.add(currentTrackSeg); 460 } 461 break; 462 case TRK: 463 switch (localName) { 464 case "trk": 465 currentState = states.pop(); 466 convertUrlToLink(currentTrackAttr); 467 data.tracks.add(new ImmutableGpxTrack(currentTrack, currentTrackAttr)); 468 break; 469 case "name": 470 case "cmt": 471 case "desc": 472 case "src": 473 case "type": 474 case "number": 475 case "url": 476 case "urlname": 477 currentTrackAttr.put(localName, accumulator.toString()); 478 break; 479 default: // Do nothing 480 } 481 break; 482 case EXT: 483 if ("extensions".equals(localName)) { 484 currentState = states.pop(); 485 } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) { 486 // only interested in extensions written by JOSM 487 currentExtensions.put(localName, accumulator.toString()); 488 } 489 break; 490 default: 491 switch (localName) { 492 case "wpt": 493 currentState = states.pop(); 494 break; 495 case "rte": 496 currentState = states.pop(); 497 convertUrlToLink(currentRoute.attr); 498 data.routes.add(currentRoute); 499 break; 500 default: // Do nothing 501 } 502 } 503 } 504 505 @Override 506 public void endDocument() throws SAXException { 507 if (!states.empty()) 508 throw new SAXException(tr("Parse error: invalid document structure for GPX document.")); 509 Extensions metaExt = (Extensions) data.get(META_EXTENSIONS); 510 if (metaExt != null && "true".equals(metaExt.get("from-server"))) { 511 data.fromServer = true; 512 } 513 gpxData = data; 514 } 515 516 /** 517 * convert url/urlname to link element (GPX 1.0 -> GPX 1.1). 518 * @param attr attributes 519 */ 520 private void convertUrlToLink(Map<String, Object> attr) { 521 String url = (String) attr.get("url"); 522 String urlname = (String) attr.get("urlname"); 523 if (url != null) { 524 if (!attr.containsKey(META_LINKS)) { 525 attr.put(META_LINKS, new LinkedList<GpxLink>()); 526 } 527 GpxLink link = new GpxLink(url); 528 link.text = urlname; 529 @SuppressWarnings("unchecked") 530 Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS); 531 links.add(link); 532 } 533 } 534 535 void tryToFinish() throws SAXException { 536 List<String> remainingElements = new ArrayList<>(elements); 537 for (int i = remainingElements.size() - 1; i >= 0; i--) { 538 endElement(null, remainingElements.get(i), remainingElements.get(i)); 539 } 540 endDocument(); 541 } 542 } 543 544 /** 545 * Constructs a new {@code GpxReader}, which can later parse the input stream 546 * and store the result in trackData and markerData 547 * 548 * @param source the source input stream 549 * @throws IOException if an IO error occurs, e.g. the input stream is closed. 550 */ 551 public GpxReader(InputStream source) throws IOException { 552 Reader utf8stream = UTFInputStreamReader.create(source); 553 Reader filtered = new InvalidXmlCharacterFilter(utf8stream); 554 this.inputSource = new InputSource(filtered); 555 } 556 557 /** 558 * Parse the GPX data. 559 * 560 * @param tryToFinish true, if the reader should return at least part of the GPX 561 * data in case of an error. 562 * @return true if file was properly parsed, false if there was error during 563 * parsing but some data were parsed anyway 564 * @throws SAXException if any SAX parsing error occurs 565 * @throws IOException if any I/O error occurs 566 */ 567 public boolean parse(boolean tryToFinish) throws SAXException, IOException { 568 Parser parser = new Parser(); 569 try { 570 Utils.parseSafeSAX(inputSource, parser); 571 return true; 572 } catch (SAXException e) { 573 if (tryToFinish) { 574 parser.tryToFinish(); 575 if (parser.data.isEmpty()) 576 throw e; 577 String message = e.getMessage(); 578 if (e instanceof SAXParseException) { 579 SAXParseException spe = (SAXParseException) e; 580 message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber()); 581 } 582 Main.warn(message); 583 return false; 584 } else 585 throw e; 586 } catch (ParserConfigurationException e) { 587 Main.error(e); // broken SAXException chaining 588 throw new SAXException(e); 589 } 590 } 591 592 /** 593 * Replies the GPX data. 594 * @return The GPX data 595 */ 596 public GpxData getGpxData() { 597 return gpxData; 598 } 599}