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