source: josm/trunk/src/org/openstreetmap/josm/io/GpxReader.java@ 8375

Last change on this file since 8375 was 8347, checked in by Don-vip, 9 years ago

simplify/instrument SAX code

  • Property svn:eol-style set to native
File size: 21.4 KB
Line 
1//License: GPL. For details, see LICENSE file.
2
3//TODO: this is far from complete, but can emulate old RawGps behaviour
4package org.openstreetmap.josm.io;
5
6import static org.openstreetmap.josm.tools.I18n.tr;
7
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.Reader;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.HashMap;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Map;
17import java.util.Stack;
18
19import javax.xml.parsers.ParserConfigurationException;
20
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.data.Bounds;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.gpx.Extensions;
25import org.openstreetmap.josm.data.gpx.GpxConstants;
26import org.openstreetmap.josm.data.gpx.GpxData;
27import org.openstreetmap.josm.data.gpx.GpxLink;
28import org.openstreetmap.josm.data.gpx.GpxRoute;
29import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
30import org.openstreetmap.josm.data.gpx.WayPoint;
31import org.openstreetmap.josm.tools.Utils;
32import org.xml.sax.Attributes;
33import org.xml.sax.InputSource;
34import org.xml.sax.SAXException;
35import org.xml.sax.SAXParseException;
36import org.xml.sax.helpers.DefaultHandler;
37
38/**
39 * Read a gpx file.
40 *
41 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br>
42 * Both GPX version 1.0 and 1.1 are supported.
43 *
44 * @author imi, ramack
45 */
46public class GpxReader implements GpxConstants {
47
48 private String version;
49 /**
50 * The resulting gpx data
51 */
52 private GpxData gpxData;
53 private enum State { init, gpx, metadata, wpt, rte, trk, ext, author, link, trkseg, copyright}
54 private InputSource inputSource;
55
56 private class Parser extends DefaultHandler {
57
58 private GpxData data;
59 private Collection<Collection<WayPoint>> currentTrack;
60 private Map<String, Object> currentTrackAttr;
61 private Collection<WayPoint> currentTrackSeg;
62 private GpxRoute currentRoute;
63 private WayPoint currentWayPoint;
64
65 private State currentState = State.init;
66
67 private GpxLink currentLink;
68 private Extensions currentExtensions;
69 private Stack<State> states;
70 private final Stack<String> elements = new Stack<>();
71
72 private StringBuffer accumulator = new StringBuffer();
73
74 private boolean nokiaSportsTrackerBug = false;
75
76 @Override
77 public void startDocument() {
78 accumulator = new StringBuffer();
79 states = new Stack<>();
80 data = new GpxData();
81 }
82
83 private double parseCoord(String s) {
84 try {
85 return Double.parseDouble(s);
86 } catch (NumberFormatException ex) {
87 return Double.NaN;
88 }
89 }
90
91 private LatLon parseLatLon(Attributes atts) {
92 return new LatLon(
93 parseCoord(atts.getValue("lat")),
94 parseCoord(atts.getValue("lon")));
95 }
96
97 @Override
98 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
99 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 -&gt; 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 Utils.parseSafeSAX(inputSource, parser);
545 return true;
546 } catch (SAXException e) {
547 if (tryToFinish) {
548 parser.tryToFinish();
549 if (parser.data.isEmpty())
550 throw e;
551 String message = e.getMessage();
552 if (e instanceof SAXParseException) {
553 SAXParseException spe = (SAXParseException)e;
554 message += " " + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber());
555 }
556 Main.warn(message);
557 return false;
558 } else
559 throw e;
560 } catch (ParserConfigurationException e) {
561 Main.error(e); // broken SAXException chaining
562 throw new SAXException(e);
563 }
564 }
565
566 /**
567 * Replies the GPX data.
568 * @return The GPX data
569 */
570 public GpxData getGpxData() {
571 return gpxData;
572 }
573}
Note: See TracBrowser for help on using the repository browser.