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}