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<String>(); 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<State>(); 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 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 096 elements.push(localName); 097 switch(currentState) { 098 case init: 099 states.push(currentState); 100 currentState = State.gpx; 101 data.creator = atts.getValue("creator"); 102 version = atts.getValue("version"); 103 if (version != null && version.startsWith("1.0")) { 104 version = "1.0"; 105 } else if (!"1.1".equals(version)) { 106 // unknown version, assume 1.1 107 version = "1.1"; 108 } 109 break; 110 case gpx: 111 if (localName.equals("metadata")) { 112 states.push(currentState); 113 currentState = State.metadata; 114 } else if (localName.equals("wpt")) { 115 states.push(currentState); 116 currentState = State.wpt; 117 currentWayPoint = new WayPoint(parseLatLon(atts)); 118 } else if (localName.equals("rte")) { 119 states.push(currentState); 120 currentState = State.rte; 121 currentRoute = new GpxRoute(); 122 } else if (localName.equals("trk")) { 123 states.push(currentState); 124 currentState = State.trk; 125 currentTrack = new ArrayList<Collection<WayPoint>>(); 126 currentTrackAttr = new HashMap<String, Object>(); 127 } else if (localName.equals("extensions")) { 128 states.push(currentState); 129 currentState = State.ext; 130 currentExtensions = new Extensions(); 131 } else if (localName.equals("gpx") && atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) { 132 nokiaSportsTrackerBug = true; 133 } 134 break; 135 case metadata: 136 if (localName.equals("author")) { 137 states.push(currentState); 138 currentState = State.author; 139 } else if (localName.equals("extensions")) { 140 states.push(currentState); 141 currentState = State.ext; 142 currentExtensions = new Extensions(); 143 } else if (localName.equals("copyright")) { 144 states.push(currentState); 145 currentState = State.copyright; 146 data.attr.put(META_COPYRIGHT_AUTHOR, atts.getValue("author")); 147 } else if (localName.equals("link")) { 148 states.push(currentState); 149 currentState = State.link; 150 currentLink = new GpxLink(atts.getValue("href")); 151 } 152 break; 153 case author: 154 if (localName.equals("link")) { 155 states.push(currentState); 156 currentState = State.link; 157 currentLink = new GpxLink(atts.getValue("href")); 158 } else if (localName.equals("email")) { 159 data.attr.put(META_AUTHOR_EMAIL, atts.getValue("id") + "@" + atts.getValue("domain")); 160 } 161 break; 162 case trk: 163 if (localName.equals("trkseg")) { 164 states.push(currentState); 165 currentState = State.trkseg; 166 currentTrackSeg = new ArrayList<WayPoint>(); 167 } else if (localName.equals("link")) { 168 states.push(currentState); 169 currentState = State.link; 170 currentLink = new GpxLink(atts.getValue("href")); 171 } else if (localName.equals("extensions")) { 172 states.push(currentState); 173 currentState = State.ext; 174 currentExtensions = new Extensions(); 175 } 176 break; 177 case trkseg: 178 if (localName.equals("trkpt")) { 179 states.push(currentState); 180 currentState = State.wpt; 181 currentWayPoint = new WayPoint(parseLatLon(atts)); 182 } 183 break; 184 case wpt: 185 if (localName.equals("link")) { 186 states.push(currentState); 187 currentState = State.link; 188 currentLink = new GpxLink(atts.getValue("href")); 189 } else if (localName.equals("extensions")) { 190 states.push(currentState); 191 currentState = State.ext; 192 currentExtensions = new Extensions(); 193 } 194 break; 195 case rte: 196 if (localName.equals("link")) { 197 states.push(currentState); 198 currentState = State.link; 199 currentLink = new GpxLink(atts.getValue("href")); 200 } else if (localName.equals("rtept")) { 201 states.push(currentState); 202 currentState = State.wpt; 203 currentWayPoint = new WayPoint(parseLatLon(atts)); 204 } else if (localName.equals("extensions")) { 205 states.push(currentState); 206 currentState = State.ext; 207 currentExtensions = new Extensions(); 208 } 209 break; 210 } 211 accumulator.setLength(0); 212 } 213 214 @Override public void characters(char[] ch, int start, int length) { 215 /** 216 * Remove illegal characters generated by the Nokia Sports Tracker device. 217 * Don't do this crude substitution for all files, since it would destroy 218 * certain unicode characters. 219 */ 220 if (nokiaSportsTrackerBug) { 221 for (int i=0; i<ch.length; ++i) { 222 if (ch[i] == 1) { 223 ch[i] = 32; 224 } 225 } 226 nokiaSportsTrackerBug = false; 227 } 228 229 accumulator.append(ch, start, length); 230 } 231 232 private Map<String, Object> getAttr() { 233 switch (currentState) { 234 case rte: return currentRoute.attr; 235 case metadata: return data.attr; 236 case wpt: return currentWayPoint.attr; 237 case trk: return currentTrackAttr; 238 default: return null; 239 } 240 } 241 242 @SuppressWarnings("unchecked") 243 @Override public void endElement(String namespaceURI, String localName, String qName) { 244 elements.pop(); 245 switch (currentState) { 246 case gpx: // GPX 1.0 247 case metadata: // GPX 1.1 248 if (localName.equals("name")) { 249 data.attr.put(META_NAME, accumulator.toString()); 250 } else if (localName.equals("desc")) { 251 data.attr.put(META_DESC, accumulator.toString()); 252 } else if (localName.equals("time")) { 253 data.attr.put(META_TIME, accumulator.toString()); 254 } else if (localName.equals("keywords")) { 255 data.attr.put(META_KEYWORDS, accumulator.toString()); 256 } else if (version.equals("1.0") && localName.equals("author")) { 257 // author is a string in 1.0, but complex element in 1.1 258 data.attr.put(META_AUTHOR_NAME, accumulator.toString()); 259 } else if (version.equals("1.0") && localName.equals("email")) { 260 data.attr.put(META_AUTHOR_EMAIL, accumulator.toString()); 261 } else if (localName.equals("url") || localName.equals("urlname")) { 262 data.attr.put(localName, accumulator.toString()); 263 } else if ((currentState == State.metadata && localName.equals("metadata")) || 264 (currentState == State.gpx && localName.equals("gpx"))) { 265 convertUrlToLink(data.attr); 266 if (currentExtensions != null && !currentExtensions.isEmpty()) { 267 data.attr.put(META_EXTENSIONS, currentExtensions); 268 } 269 currentState = states.pop(); 270 } 271 //TODO: parse bounds, extensions 272 break; 273 case author: 274 if (localName.equals("author")) { 275 currentState = states.pop(); 276 } else if (localName.equals("name")) { 277 data.attr.put(META_AUTHOR_NAME, accumulator.toString()); 278 } else if (localName.equals("email")) { 279 // do nothing, has been parsed on startElement 280 } else if (localName.equals("link")) { 281 data.attr.put(META_AUTHOR_LINK, currentLink); 282 } 283 break; 284 case copyright: 285 if (localName.equals("copyright")) { 286 currentState = states.pop(); 287 } else if (localName.equals("year")) { 288 data.attr.put(META_COPYRIGHT_YEAR, accumulator.toString()); 289 } else if (localName.equals("license")) { 290 data.attr.put(META_COPYRIGHT_LICENSE, accumulator.toString()); 291 } 292 break; 293 case link: 294 if (localName.equals("text")) { 295 currentLink.text = accumulator.toString(); 296 } else if (localName.equals("type")) { 297 currentLink.type = accumulator.toString(); 298 } else if (localName.equals("link")) { 299 if (currentLink.uri == null && accumulator != null && accumulator.toString().length() != 0) { 300 currentLink = new GpxLink(accumulator.toString()); 301 } 302 currentState = states.pop(); 303 } 304 if (currentState == State.author) { 305 data.attr.put(META_AUTHOR_LINK, currentLink); 306 } else if (currentState != State.link) { 307 Map<String, Object> attr = getAttr(); 308 if (!attr.containsKey(META_LINKS)) { 309 attr.put(META_LINKS, new LinkedList<GpxLink>()); 310 } 311 ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink); 312 } 313 break; 314 case wpt: 315 if ( localName.equals("ele") || localName.equals("magvar") 316 || localName.equals("name") || localName.equals("src") 317 || localName.equals("geoidheight") || localName.equals("type") 318 || localName.equals("sym") || localName.equals("url") 319 || localName.equals("urlname")) { 320 currentWayPoint.attr.put(localName, accumulator.toString()); 321 } else if(localName.equals("hdop") || localName.equals("vdop") || 322 localName.equals("pdop")) { 323 try { 324 currentWayPoint.attr.put(localName, Float.parseFloat(accumulator.toString())); 325 } catch(Exception e) { 326 currentWayPoint.attr.put(localName, new Float(0)); 327 } 328 } else if (localName.equals("time")) { 329 currentWayPoint.attr.put(localName, accumulator.toString()); 330 currentWayPoint.setTime(); 331 } else if (localName.equals("cmt") || localName.equals("desc")) { 332 currentWayPoint.attr.put(localName, accumulator.toString()); 333 currentWayPoint.setTime(); 334 } else if (localName.equals("rtept")) { 335 currentState = states.pop(); 336 convertUrlToLink(currentWayPoint.attr); 337 currentRoute.routePoints.add(currentWayPoint); 338 } else if (localName.equals("trkpt")) { 339 currentState = states.pop(); 340 convertUrlToLink(currentWayPoint.attr); 341 currentTrackSeg.add(currentWayPoint); 342 } else if (localName.equals("wpt")) { 343 currentState = states.pop(); 344 convertUrlToLink(currentWayPoint.attr); 345 if (currentExtensions != null && !currentExtensions.isEmpty()) { 346 currentWayPoint.attr.put(META_EXTENSIONS, currentExtensions); 347 } 348 data.waypoints.add(currentWayPoint); 349 } 350 break; 351 case trkseg: 352 if (localName.equals("trkseg")) { 353 currentState = states.pop(); 354 currentTrack.add(currentTrackSeg); 355 } 356 break; 357 case trk: 358 if (localName.equals("trk")) { 359 currentState = states.pop(); 360 convertUrlToLink(currentTrackAttr); 361 data.tracks.add(new ImmutableGpxTrack(currentTrack, currentTrackAttr)); 362 } else if (localName.equals("name") || localName.equals("cmt") 363 || localName.equals("desc") || localName.equals("src") 364 || localName.equals("type") || localName.equals("number") 365 || localName.equals("url") || localName.equals("urlname")) { 366 currentTrackAttr.put(localName, accumulator.toString()); 367 } 368 break; 369 case ext: 370 if (localName.equals("extensions")) { 371 currentState = states.pop(); 372 // only interested in extensions written by JOSM 373 } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) { 374 currentExtensions.put(localName, accumulator.toString()); 375 } 376 break; 377 default: 378 if (localName.equals("wpt")) { 379 currentState = states.pop(); 380 } else if (localName.equals("rte")) { 381 currentState = states.pop(); 382 convertUrlToLink(currentRoute.attr); 383 data.routes.add(currentRoute); 384 } 385 } 386 } 387 388 @Override public void endDocument() throws SAXException { 389 if (!states.empty()) 390 throw new SAXException(tr("Parse error: invalid document structure for GPX document.")); 391 Extensions metaExt = (Extensions) data.attr.get(META_EXTENSIONS); 392 if (metaExt != null && "true".equals(metaExt.get("from-server"))) { 393 data.fromServer = true; 394 } 395 gpxData = data; 396 } 397 398 /** 399 * convert url/urlname to link element (GPX 1.0 -> GPX 1.1). 400 */ 401 private void convertUrlToLink(Map<String, Object> attr) { 402 String url = (String) attr.get("url"); 403 String urlname = (String) attr.get("urlname"); 404 if (url != null) { 405 if (!attr.containsKey(META_LINKS)) { 406 attr.put(META_LINKS, new LinkedList<GpxLink>()); 407 } 408 GpxLink link = new GpxLink(url); 409 link.text = urlname; 410 @SuppressWarnings({ "unchecked", "rawtypes" }) 411 Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS); 412 links.add(link); 413 } 414 } 415 416 public void tryToFinish() throws SAXException { 417 List<String> remainingElements = new ArrayList<String>(elements); 418 for (int i=remainingElements.size() - 1; i >= 0; i--) { 419 endElement(null, remainingElements.get(i), remainingElements.get(i)); 420 } 421 endDocument(); 422 } 423 } 424 425 /** 426 * Parse the input stream and store the result in trackData and markerData 427 * 428 * @param source the source input stream 429 * @throws IOException if an IO error occurs, e.g. the input stream is closed. 430 */ 431 public GpxReader(InputStream source) throws IOException { 432 Reader utf8stream = UTFInputStreamReader.create(source, "UTF-8"); 433 Reader filtered = new InvalidXmlCharacterFilter(utf8stream); 434 this.inputSource = new InputSource(filtered); 435 } 436 437 /** 438 * Parse the GPX data. 439 * 440 * @param tryToFinish true, if the reader should return at least part of the GPX 441 * data in case of an error. 442 * @return true if file was properly parsed, false if there was error during 443 * parsing but some data were parsed anyway 444 * @throws SAXException 445 * @throws IOException 446 */ 447 public boolean parse(boolean tryToFinish) throws SAXException, IOException { 448 Parser parser = new Parser(); 449 try { 450 SAXParserFactory factory = SAXParserFactory.newInstance(); 451 factory.setNamespaceAware(true); 452 factory.newSAXParser().parse(inputSource, parser); 453 return true; 454 } catch (SAXException e) { 455 if (tryToFinish) { 456 parser.tryToFinish(); 457 if (parser.data.isEmpty()) 458 throw e; 459 String message = e.getMessage(); 460 if (e instanceof SAXParseException) { 461 SAXParseException spe = ((SAXParseException)e); 462 message += " " + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber()); 463 } 464 Main.warn(message); 465 return false; 466 } else 467 throw e; 468 } catch (ParserConfigurationException e) { 469 e.printStackTrace(); // broken SAXException chaining 470 throw new SAXException(e); 471 } 472 } 473 474 /** 475 * Replies the GPX data. 476 * @return The GPX data 477 */ 478 public GpxData getGpxData() { 479 return gpxData; 480 } 481}