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