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    }