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 != null && !attr.containsKey(META_LINKS)) { 402 attr.put(META_LINKS, new LinkedList<GpxLink>()); 403 } 404 if (attr != null) 405 ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink); 406 } 407 break; 408 case WPT: 409 switch (localName) { 410 case "ele": 411 case "magvar": 412 case "name": 413 case "src": 414 case "geoidheight": 415 case "type": 416 case "sym": 417 case "url": 418 case "urlname": 419 currentWayPoint.put(localName, accumulator.toString()); 420 break; 421 case "hdop": 422 case "vdop": 423 case "pdop": 424 try { 425 currentWayPoint.put(localName, Float.valueOf(accumulator.toString())); 426 } catch (NumberFormatException e) { 427 currentWayPoint.put(localName, 0f); 428 } 429 break; 430 case "time": 431 case "cmt": 432 case "desc": 433 currentWayPoint.put(localName, accumulator.toString()); 434 currentWayPoint.setTime(); 435 break; 436 case "rtept": 437 currentState = states.pop(); 438 convertUrlToLink(currentWayPoint.attr); 439 currentRoute.routePoints.add(currentWayPoint); 440 break; 441 case "trkpt": 442 currentState = states.pop(); 443 convertUrlToLink(currentWayPoint.attr); 444 currentTrackSeg.add(currentWayPoint); 445 break; 446 case "wpt": 447 currentState = states.pop(); 448 convertUrlToLink(currentWayPoint.attr); 449 if (currentExtensions != null && !currentExtensions.isEmpty()) { 450 currentWayPoint.put(META_EXTENSIONS, currentExtensions); 451 } 452 data.waypoints.add(currentWayPoint); 453 break; 454 default: // Do nothing 455 } 456 break; 457 case TRKSEG: 458 if ("trkseg".equals(localName)) { 459 currentState = states.pop(); 460 currentTrack.add(currentTrackSeg); 461 } 462 break; 463 case TRK: 464 switch (localName) { 465 case "trk": 466 currentState = states.pop(); 467 convertUrlToLink(currentTrackAttr); 468 data.tracks.add(new ImmutableGpxTrack(currentTrack, currentTrackAttr)); 469 break; 470 case "name": 471 case "cmt": 472 case "desc": 473 case "src": 474 case "type": 475 case "number": 476 case "url": 477 case "urlname": 478 currentTrackAttr.put(localName, accumulator.toString()); 479 break; 480 default: // Do nothing 481 } 482 break; 483 case EXT: 484 if ("extensions".equals(localName)) { 485 currentState = states.pop(); 486 } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) { 487 // only interested in extensions written by JOSM 488 currentExtensions.put(localName, accumulator.toString()); 489 } 490 break; 491 default: 492 switch (localName) { 493 case "wpt": 494 currentState = states.pop(); 495 break; 496 case "rte": 497 currentState = states.pop(); 498 convertUrlToLink(currentRoute.attr); 499 data.routes.add(currentRoute); 500 break; 501 default: // Do nothing 502 } 503 } 504 } 505 506 @Override 507 public void endDocument() throws SAXException { 508 if (!states.empty()) 509 throw new SAXException(tr("Parse error: invalid document structure for GPX document.")); 510 Extensions metaExt = (Extensions) data.get(META_EXTENSIONS); 511 if (metaExt != null && "true".equals(metaExt.get("from-server"))) { 512 data.fromServer = true; 513 } 514 gpxData = data; 515 } 516 517 /** 518 * convert url/urlname to link element (GPX 1.0 -> GPX 1.1). 519 * @param attr attributes 520 */ 521 private void convertUrlToLink(Map<String, Object> attr) { 522 String url = (String) attr.get("url"); 523 String urlname = (String) attr.get("urlname"); 524 if (url != null) { 525 if (!attr.containsKey(META_LINKS)) { 526 attr.put(META_LINKS, new LinkedList<GpxLink>()); 527 } 528 GpxLink link = new GpxLink(url); 529 link.text = urlname; 530 @SuppressWarnings("unchecked") 531 Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS); 532 links.add(link); 533 } 534 } 535 536 void tryToFinish() throws SAXException { 537 List<String> remainingElements = new ArrayList<>(elements); 538 for (int i = remainingElements.size() - 1; i >= 0; i--) { 539 endElement(null, remainingElements.get(i), remainingElements.get(i)); 540 } 541 endDocument(); 542 } 543 } 544 545 /** 546 * Constructs a new {@code GpxReader}, which can later parse the input stream 547 * and store the result in trackData and markerData 548 * 549 * @param source the source input stream 550 * @throws IOException if an IO error occurs, e.g. the input stream is closed. 551 */ 552 public GpxReader(InputStream source) throws IOException { 553 Reader utf8stream = UTFInputStreamReader.create(source); 554 Reader filtered = new InvalidXmlCharacterFilter(utf8stream); 555 this.inputSource = new InputSource(filtered); 556 } 557 558 /** 559 * Parse the GPX data. 560 * 561 * @param tryToFinish true, if the reader should return at least part of the GPX 562 * data in case of an error. 563 * @return true if file was properly parsed, false if there was error during 564 * parsing but some data were parsed anyway 565 * @throws SAXException if any SAX parsing error occurs 566 * @throws IOException if any I/O error occurs 567 */ 568 public boolean parse(boolean tryToFinish) throws SAXException, IOException { 569 Parser parser = new Parser(); 570 try { 571 Utils.parseSafeSAX(inputSource, parser); 572 return true; 573 } catch (SAXException e) { 574 if (tryToFinish) { 575 parser.tryToFinish(); 576 if (parser.data.isEmpty()) 577 throw e; 578 String message = e.getMessage(); 579 if (e instanceof SAXParseException) { 580 SAXParseException spe = (SAXParseException) e; 581 message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber()); 582 } 583 Main.warn(message); 584 return false; 585 } else 586 throw e; 587 } catch (ParserConfigurationException e) { 588 Main.error(e); // broken SAXException chaining 589 throw new SAXException(e); 590 } 591 } 592 593 /** 594 * Replies the GPX data. 595 * @return The GPX data 596 */ 597 public GpxData getGpxData() { 598 return gpxData; 599 } 600}