001 //License: GPL. Copyright 2007 by Immanuel Scholz and others 002 003 //TODO: this is far from complete, but can emulate old RawGps behaviour 004 package org.openstreetmap.josm.io; 005 006 import static org.openstreetmap.josm.tools.I18n.tr; 007 008 import java.io.IOException; 009 import java.io.InputStream; 010 import java.util.ArrayList; 011 import java.util.Collection; 012 import java.util.HashMap; 013 import java.util.LinkedList; 014 import java.util.List; 015 import java.util.Map; 016 import java.util.Stack; 017 018 import javax.xml.parsers.ParserConfigurationException; 019 import javax.xml.parsers.SAXParserFactory; 020 021 import org.openstreetmap.josm.data.coor.LatLon; 022 import org.openstreetmap.josm.data.gpx.GpxData; 023 import org.openstreetmap.josm.data.gpx.GpxLink; 024 import org.openstreetmap.josm.data.gpx.GpxRoute; 025 import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack; 026 import org.openstreetmap.josm.data.gpx.WayPoint; 027 import org.xml.sax.Attributes; 028 import org.xml.sax.InputSource; 029 import org.xml.sax.SAXException; 030 import org.xml.sax.helpers.DefaultHandler; 031 032 /** 033 * Read a gpx file. 034 * 035 * Bounds are not read, as we caluclate them. @see GpxData.recalculateBounds() 036 * Both GPX version 1.0 and 1.1 are supported. 037 * 038 * @author imi, ramack 039 */ 040 public class GpxReader { 041 042 private String version; 043 /** 044 * The resulting gpx data 045 */ 046 public GpxData data; 047 private enum State { init, gpx, metadata, wpt, rte, trk, ext, author, link, trkseg, copyright} 048 private InputSource inputSource; 049 050 private class Parser extends DefaultHandler { 051 052 private GpxData currentData; 053 private Collection<Collection<WayPoint>> currentTrack; 054 private Map<String, Object> currentTrackAttr; 055 private Collection<WayPoint> currentTrackSeg; 056 private GpxRoute currentRoute; 057 private WayPoint currentWayPoint; 058 059 private State currentState = State.init; 060 061 private GpxLink currentLink; 062 private Stack<State> states; 063 private final Stack<String> elements = new Stack<String>(); 064 065 private StringBuffer accumulator = new StringBuffer(); 066 067 private boolean nokiaSportsTrackerBug = false; 068 069 @Override public void startDocument() { 070 accumulator = new StringBuffer(); 071 states = new Stack<State>(); 072 currentData = new GpxData(); 073 } 074 075 private double parseCoord(String s) { 076 try { 077 return Double.parseDouble(s); 078 } catch (NumberFormatException ex) { 079 return Double.NaN; 080 } 081 } 082 083 private LatLon parseLatLon(Attributes atts) { 084 return new LatLon( 085 parseCoord(atts.getValue("lat")), 086 parseCoord(atts.getValue("lon"))); 087 } 088 089 @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 090 elements.push(qName); 091 switch(currentState) { 092 case init: 093 states.push(currentState); 094 currentState = State.gpx; 095 currentData.creator = atts.getValue("creator"); 096 version = atts.getValue("version"); 097 if (version != null && version.startsWith("1.0")) { 098 version = "1.0"; 099 } else if (!"1.1".equals(version)) { 100 // unknown version, assume 1.1 101 version = "1.1"; 102 } 103 break; 104 case gpx: 105 if (qName.equals("metadata")) { 106 states.push(currentState); 107 currentState = State.metadata; 108 } else if (qName.equals("wpt")) { 109 states.push(currentState); 110 currentState = State.wpt; 111 currentWayPoint = new WayPoint(parseLatLon(atts)); 112 } else if (qName.equals("rte")) { 113 states.push(currentState); 114 currentState = State.rte; 115 currentRoute = new GpxRoute(); 116 } else if (qName.equals("trk")) { 117 states.push(currentState); 118 currentState = State.trk; 119 currentTrack = new ArrayList<Collection<WayPoint>>(); 120 currentTrackAttr = new HashMap<String, Object>(); 121 } else if (qName.equals("extensions")) { 122 states.push(currentState); 123 currentState = State.ext; 124 } else if (qName.equals("gpx") && atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) { 125 nokiaSportsTrackerBug = true; 126 } 127 break; 128 case metadata: 129 if (qName.equals("author")) { 130 states.push(currentState); 131 currentState = State.author; 132 } else if (qName.equals("extensions")) { 133 states.push(currentState); 134 currentState = State.ext; 135 } else if (qName.equals("copyright")) { 136 states.push(currentState); 137 currentState = State.copyright; 138 currentData.attr.put(GpxData.META_COPYRIGHT_AUTHOR, atts.getValue("author")); 139 } else if (qName.equals("link")) { 140 states.push(currentState); 141 currentState = State.link; 142 currentLink = new GpxLink(atts.getValue("href")); 143 } 144 break; 145 case author: 146 if (qName.equals("link")) { 147 states.push(currentState); 148 currentState = State.link; 149 currentLink = new GpxLink(atts.getValue("href")); 150 } else if (qName.equals("email")) { 151 currentData.attr.put(GpxData.META_AUTHOR_EMAIL, atts.getValue("id") + "@" + atts.getValue("domain")); 152 } 153 break; 154 case trk: 155 if (qName.equals("trkseg")) { 156 states.push(currentState); 157 currentState = State.trkseg; 158 currentTrackSeg = new ArrayList<WayPoint>(); 159 } else if (qName.equals("link")) { 160 states.push(currentState); 161 currentState = State.link; 162 currentLink = new GpxLink(atts.getValue("href")); 163 } else if (qName.equals("extensions")) { 164 states.push(currentState); 165 currentState = State.ext; 166 } 167 break; 168 case trkseg: 169 if (qName.equals("trkpt")) { 170 states.push(currentState); 171 currentState = State.wpt; 172 currentWayPoint = new WayPoint(parseLatLon(atts)); 173 } 174 break; 175 case wpt: 176 if (qName.equals("link")) { 177 states.push(currentState); 178 currentState = State.link; 179 currentLink = new GpxLink(atts.getValue("href")); 180 } else if (qName.equals("extensions")) { 181 states.push(currentState); 182 currentState = State.ext; 183 } 184 break; 185 case rte: 186 if (qName.equals("link")) { 187 states.push(currentState); 188 currentState = State.link; 189 currentLink = new GpxLink(atts.getValue("href")); 190 } else if (qName.equals("rtept")) { 191 states.push(currentState); 192 currentState = State.wpt; 193 currentWayPoint = new WayPoint(parseLatLon(atts)); 194 } else if (qName.equals("extensions")) { 195 states.push(currentState); 196 currentState = State.ext; 197 } 198 break; 199 default: 200 } 201 accumulator.setLength(0); 202 } 203 204 @Override public void characters(char[] ch, int start, int length) { 205 /** 206 * Remove illegal characters generated by the Nokia Sports Tracker device. 207 * Don't do this crude substitution for all files, since it would destroy 208 * certain unicode characters. 209 */ 210 if (nokiaSportsTrackerBug) { 211 for (int i=0; i<ch.length; ++i) { 212 if (ch[i] == 1) { 213 ch[i] = 32; 214 } 215 } 216 nokiaSportsTrackerBug = false; 217 } 218 219 accumulator.append(ch, start, length); 220 } 221 222 private Map<String, Object> getAttr() { 223 switch (currentState) { 224 case rte: return currentRoute.attr; 225 case metadata: return currentData.attr; 226 case wpt: return currentWayPoint.attr; 227 case trk: return currentTrackAttr; 228 default: return null; 229 } 230 } 231 232 @SuppressWarnings("unchecked") 233 @Override public void endElement(String namespaceURI, String localName, String qName) { 234 elements.pop(); 235 switch (currentState) { 236 case gpx: // GPX 1.0 237 case metadata: // GPX 1.1 238 if (qName.equals("name")) { 239 currentData.attr.put(GpxData.META_NAME, accumulator.toString()); 240 } else if (qName.equals("desc")) { 241 currentData.attr.put(GpxData.META_DESC, accumulator.toString()); 242 } else if (qName.equals("time")) { 243 currentData.attr.put(GpxData.META_TIME, accumulator.toString()); 244 } else if (qName.equals("keywords")) { 245 currentData.attr.put(GpxData.META_KEYWORDS, accumulator.toString()); 246 } else if (version.equals("1.0") && qName.equals("author")) { 247 // author is a string in 1.0, but complex element in 1.1 248 currentData.attr.put(GpxData.META_AUTHOR_NAME, accumulator.toString()); 249 } else if (version.equals("1.0") && qName.equals("email")) { 250 currentData.attr.put(GpxData.META_AUTHOR_EMAIL, accumulator.toString()); 251 } else if (qName.equals("url") || qName.equals("urlname")) { 252 currentData.attr.put(qName, accumulator.toString()); 253 } else if ((currentState == State.metadata && qName.equals("metadata")) || 254 (currentState == State.gpx && qName.equals("gpx"))) { 255 convertUrlToLink(currentData.attr); 256 currentState = states.pop(); 257 } 258 //TODO: parse bounds, extensions 259 break; 260 case author: 261 if (qName.equals("author")) { 262 currentState = states.pop(); 263 } else if (qName.equals("name")) { 264 currentData.attr.put(GpxData.META_AUTHOR_NAME, accumulator.toString()); 265 } else if (qName.equals("email")) { 266 // do nothing, has been parsed on startElement 267 } else if (qName.equals("link")) { 268 currentData.attr.put(GpxData.META_AUTHOR_LINK, currentLink); 269 } 270 break; 271 case copyright: 272 if (qName.equals("copyright")) { 273 currentState = states.pop(); 274 } else if (qName.equals("year")) { 275 currentData.attr.put(GpxData.META_COPYRIGHT_YEAR, accumulator.toString()); 276 } else if (qName.equals("license")) { 277 currentData.attr.put(GpxData.META_COPYRIGHT_LICENSE, accumulator.toString()); 278 } 279 break; 280 case link: 281 if (qName.equals("text")) { 282 currentLink.text = accumulator.toString(); 283 } else if (qName.equals("type")) { 284 currentLink.type = accumulator.toString(); 285 } else if (qName.equals("link")) { 286 if (currentLink.uri == null && accumulator != null && accumulator.toString().length() != 0) { 287 currentLink = new GpxLink(accumulator.toString()); 288 } 289 currentState = states.pop(); 290 } 291 if (currentState == State.author) { 292 currentData.attr.put(GpxData.META_AUTHOR_LINK, currentLink); 293 } else if (currentState != State.link) { 294 Map<String, Object> attr = getAttr(); 295 if (!attr.containsKey(GpxData.META_LINKS)) { 296 attr.put(GpxData.META_LINKS, new LinkedList<GpxLink>()); 297 } 298 ((Collection<GpxLink>) attr.get(GpxData.META_LINKS)).add(currentLink); 299 } 300 break; 301 case wpt: 302 if ( qName.equals("ele") || qName.equals("magvar") 303 || qName.equals("name") || qName.equals("src") 304 || qName.equals("geoidheight") || qName.equals("type") 305 || qName.equals("sym") || qName.equals("url") 306 || qName.equals("urlname")) { 307 currentWayPoint.attr.put(qName, accumulator.toString()); 308 } else if(qName.equals("hdop") || qName.equals("vdop") || 309 qName.equals("pdop")) { 310 try { 311 currentWayPoint.attr.put(qName, Float.parseFloat(accumulator.toString())); 312 } catch(Exception e) { 313 currentWayPoint.attr.put(qName, new Float(0)); 314 } 315 } else if (qName.equals("time")) { 316 currentWayPoint.attr.put(qName, accumulator.toString()); 317 currentWayPoint.setTime(); 318 } else if (qName.equals("cmt") || qName.equals("desc")) { 319 currentWayPoint.attr.put(qName, accumulator.toString()); 320 currentWayPoint.setTime(); 321 } else if (qName.equals("rtept")) { 322 currentState = states.pop(); 323 convertUrlToLink(currentWayPoint.attr); 324 currentRoute.routePoints.add(currentWayPoint); 325 } else if (qName.equals("trkpt")) { 326 currentState = states.pop(); 327 convertUrlToLink(currentWayPoint.attr); 328 currentTrackSeg.add(currentWayPoint); 329 } else if (qName.equals("wpt")) { 330 currentState = states.pop(); 331 convertUrlToLink(currentWayPoint.attr); 332 currentData.waypoints.add(currentWayPoint); 333 } 334 break; 335 case trkseg: 336 if (qName.equals("trkseg")) { 337 currentState = states.pop(); 338 currentTrack.add(currentTrackSeg); 339 } 340 break; 341 case trk: 342 if (qName.equals("trk")) { 343 currentState = states.pop(); 344 convertUrlToLink(currentTrackAttr); 345 currentData.tracks.add(new ImmutableGpxTrack(currentTrack, currentTrackAttr)); 346 } else if (qName.equals("name") || qName.equals("cmt") 347 || qName.equals("desc") || qName.equals("src") 348 || qName.equals("type") || qName.equals("number") 349 || qName.equals("url") || qName.equals("urlname")) { 350 currentTrackAttr.put(qName, accumulator.toString()); 351 } 352 break; 353 case ext: 354 if (qName.equals("extensions")) { 355 currentState = states.pop(); 356 } 357 break; 358 default: 359 if (qName.equals("wpt")) { 360 currentState = states.pop(); 361 } else if (qName.equals("rte")) { 362 currentState = states.pop(); 363 convertUrlToLink(currentRoute.attr); 364 currentData.routes.add(currentRoute); 365 } 366 } 367 } 368 369 @Override public void endDocument() throws SAXException { 370 if (!states.empty()) 371 throw new SAXException(tr("Parse error: invalid document structure for GPX document.")); 372 data = currentData; 373 } 374 375 /** 376 * convert url/urlname to link element (GPX 1.0 -> GPX 1.1). 377 */ 378 private void convertUrlToLink(Map<String, Object> attr) { 379 String url = (String) attr.get("url"); 380 String urlname = (String) attr.get("urlname"); 381 if (url != null) { 382 if (!attr.containsKey(GpxData.META_LINKS)) { 383 attr.put(GpxData.META_LINKS, new LinkedList<GpxLink>()); 384 } 385 GpxLink link = new GpxLink(url); 386 link.text = urlname; 387 ((Collection<GpxLink>) attr.get(GpxData.META_LINKS)).add(link); 388 } 389 } 390 391 public void tryToFinish() throws SAXException { 392 List<String> remainingElements = new ArrayList<String>(elements); 393 for (int i=remainingElements.size() - 1; i >= 0; i--) { 394 endElement(null, remainingElements.get(i), remainingElements.get(i)); 395 } 396 endDocument(); 397 } 398 } 399 400 /** 401 * Parse the input stream and store the result in trackData and markerData 402 * 403 */ 404 public GpxReader(InputStream source) throws IOException { 405 this.inputSource = new InputSource(UTFInputStreamReader.create(source, "UTF-8")); 406 } 407 408 /** 409 * 410 * @return True if file was properly parsed, false if there was error during parsing but some data were parsed anyway 411 * @throws SAXException 412 * @throws IOException 413 */ 414 public boolean parse(boolean tryToFinish) throws SAXException, IOException { 415 Parser parser = new Parser(); 416 try { 417 SAXParserFactory factory = SAXParserFactory.newInstance(); 418 // support files with invalid xml namespace declarations (see #7247) 419 factory.setNamespaceAware(false); 420 factory.newSAXParser().parse(inputSource, parser); 421 return true; 422 } catch (SAXException e) { 423 if (tryToFinish) { 424 parser.tryToFinish(); 425 if (parser.currentData.isEmpty()) 426 throw e; 427 return false; 428 } else 429 throw e; 430 } catch (ParserConfigurationException e) { 431 e.printStackTrace(); // broken SAXException chaining 432 throw new SAXException(e); 433 } 434 } 435 }