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

Last change on this file since 5508 was 5398, checked in by bastiK, 12 years ago

convert gpx 1.0 to 1.1 (fixes #7927)

  • Property svn:eol-style set to native
File size: 18.3 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others
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.util.ArrayList;
11import java.util.Collection;
12import java.util.HashMap;
13import java.util.LinkedList;
14import java.util.List;
15import java.util.Map;
16import java.util.Stack;
17
18import javax.xml.parsers.ParserConfigurationException;
19import javax.xml.parsers.SAXParserFactory;
20
21import org.openstreetmap.josm.data.coor.LatLon;
22import org.openstreetmap.josm.data.gpx.GpxData;
23import org.openstreetmap.josm.data.gpx.GpxLink;
24import org.openstreetmap.josm.data.gpx.GpxRoute;
25import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
26import org.openstreetmap.josm.data.gpx.WayPoint;
27import org.xml.sax.Attributes;
28import org.xml.sax.InputSource;
29import org.xml.sax.SAXException;
30import org.xml.sax.helpers.DefaultHandler;
31
32/**
33 * Read a gpx file.
34 *
35 * Bounds are not read, as we caluclate them. @see GpxData.recalculateBounds()
36 * Both GPX version 1.0 and 1.1 are supported.
37 *
38 * @author imi, ramack
39 */
40public class GpxReader {
41
42 private String version;
43 /**
44 * The resulting gpx data
45 */
46 public GpxData data;
47 private enum State { init, gpx, metadata, wpt, rte, trk, ext, author, link, trkseg, copyright}
48 private InputSource inputSource;
49
50 private class Parser extends DefaultHandler {
51
52 private GpxData currentData;
53 private Collection<Collection<WayPoint>> currentTrack;
54 private Map<String, Object> currentTrackAttr;
55 private Collection<WayPoint> currentTrackSeg;
56 private GpxRoute currentRoute;
57 private WayPoint currentWayPoint;
58
59 private State currentState = State.init;
60
61 private GpxLink currentLink;
62 private Stack<State> states;
63 private final Stack<String> elements = new Stack<String>();
64
65 private StringBuffer accumulator = new StringBuffer();
66
67 private boolean nokiaSportsTrackerBug = false;
68
69 @Override public void startDocument() {
70 accumulator = new StringBuffer();
71 states = new Stack<State>();
72 currentData = new GpxData();
73 }
74
75 private double parseCoord(String s) {
76 try {
77 return Double.parseDouble(s);
78 } catch (NumberFormatException ex) {
79 return Double.NaN;
80 }
81 }
82
83 private LatLon parseLatLon(Attributes atts) {
84 return new LatLon(
85 parseCoord(atts.getValue("lat")),
86 parseCoord(atts.getValue("lon")));
87 }
88
89 @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
90 elements.push(qName);
91 switch(currentState) {
92 case init:
93 states.push(currentState);
94 currentState = State.gpx;
95 currentData.creator = atts.getValue("creator");
96 version = atts.getValue("version");
97 if (version != null && version.startsWith("1.0")) {
98 version = "1.0";
99 } 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}
Note: See TracBrowser for help on using the repository browser.