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