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