001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.Reader;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.HashMap;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.Map;
015import java.util.Stack;
016
017import javax.xml.parsers.ParserConfigurationException;
018
019import org.openstreetmap.josm.Main;
020import org.openstreetmap.josm.data.Bounds;
021import org.openstreetmap.josm.data.coor.LatLon;
022import org.openstreetmap.josm.data.gpx.Extensions;
023import org.openstreetmap.josm.data.gpx.GpxConstants;
024import org.openstreetmap.josm.data.gpx.GpxData;
025import org.openstreetmap.josm.data.gpx.GpxLink;
026import org.openstreetmap.josm.data.gpx.GpxRoute;
027import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
028import org.openstreetmap.josm.data.gpx.WayPoint;
029import org.openstreetmap.josm.tools.Utils;
030import org.xml.sax.Attributes;
031import org.xml.sax.InputSource;
032import org.xml.sax.SAXException;
033import org.xml.sax.SAXParseException;
034import org.xml.sax.helpers.DefaultHandler;
035
036/**
037 * Read a gpx file.
038 *
039 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br>
040 * Both GPX version 1.0 and 1.1 are supported.
041 *
042 * @author imi, ramack
043 */
044public class GpxReader implements GpxConstants {
045
046    private enum State {
047        INIT,
048        GPX,
049        METADATA,
050        WPT,
051        RTE,
052        TRK,
053        EXT,
054        AUTHOR,
055        LINK,
056        TRKSEG,
057        COPYRIGHT
058    }
059
060    private String version;
061    /** The resulting gpx data */
062    private GpxData gpxData;
063    private final InputSource inputSource;
064
065    private class Parser extends DefaultHandler {
066
067        private GpxData data;
068        private Collection<Collection<WayPoint>> currentTrack;
069        private Map<String, Object> currentTrackAttr;
070        private Collection<WayPoint> currentTrackSeg;
071        private GpxRoute currentRoute;
072        private WayPoint currentWayPoint;
073
074        private State currentState = State.INIT;
075
076        private GpxLink currentLink;
077        private Extensions currentExtensions;
078        private Stack<State> states;
079        private final Stack<String> elements = new Stack<>();
080
081        private StringBuilder accumulator = new StringBuilder();
082
083        private boolean nokiaSportsTrackerBug;
084
085        @Override
086        public void startDocument() {
087            accumulator = new StringBuilder();
088            states = new Stack<>();
089            data = new GpxData();
090        }
091
092        private double parseCoord(String s) {
093            try {
094                return Double.parseDouble(s);
095            } catch (NumberFormatException ex) {
096                return Double.NaN;
097            }
098        }
099
100        private LatLon parseLatLon(Attributes atts) {
101            return new LatLon(
102                    parseCoord(atts.getValue("lat")),
103                    parseCoord(atts.getValue("lon")));
104        }
105
106        @Override
107        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
108            elements.push(localName);
109            switch(currentState) {
110            case INIT:
111                states.push(currentState);
112                currentState = State.GPX;
113                data.creator = atts.getValue("creator");
114                version = atts.getValue("version");
115                if (version != null && version.startsWith("1.0")) {
116                    version = "1.0";
117                } else if (!"1.1".equals(version)) {
118                    // unknown version, assume 1.1
119                    version = "1.1";
120                }
121                break;
122            case GPX:
123                switch (localName) {
124                case "metadata":
125                    states.push(currentState);
126                    currentState = State.METADATA;
127                    break;
128                case "wpt":
129                    states.push(currentState);
130                    currentState = State.WPT;
131                    currentWayPoint = new WayPoint(parseLatLon(atts));
132                    break;
133                case "rte":
134                    states.push(currentState);
135                    currentState = State.RTE;
136                    currentRoute = new GpxRoute();
137                    break;
138                case "trk":
139                    states.push(currentState);
140                    currentState = State.TRK;
141                    currentTrack = new ArrayList<>();
142                    currentTrackAttr = new HashMap<>();
143                    break;
144                case "extensions":
145                    states.push(currentState);
146                    currentState = State.EXT;
147                    currentExtensions = new Extensions();
148                    break;
149                case "gpx":
150                    if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) {
151                        nokiaSportsTrackerBug = true;
152                    }
153                    break;
154                default: // Do nothing
155                }
156                break;
157            case METADATA:
158                switch (localName) {
159                case "author":
160                    states.push(currentState);
161                    currentState = State.AUTHOR;
162                    break;
163                case "extensions":
164                    states.push(currentState);
165                    currentState = State.EXT;
166                    currentExtensions = new Extensions();
167                    break;
168                case "copyright":
169                    states.push(currentState);
170                    currentState = State.COPYRIGHT;
171                    data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author"));
172                    break;
173                case "link":
174                    states.push(currentState);
175                    currentState = State.LINK;
176                    currentLink = new GpxLink(atts.getValue("href"));
177                    break;
178                case "bounds":
179                    data.put(META_BOUNDS, new Bounds(
180                                parseCoord(atts.getValue("minlat")),
181                                parseCoord(atts.getValue("minlon")),
182                                parseCoord(atts.getValue("maxlat")),
183                                parseCoord(atts.getValue("maxlon"))));
184                    break;
185                default: // Do nothing
186                }
187                break;
188            case AUTHOR:
189                switch (localName) {
190                case "link":
191                    states.push(currentState);
192                    currentState = State.LINK;
193                    currentLink = new GpxLink(atts.getValue("href"));
194                    break;
195                case "email":
196                    data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain"));
197                    break;
198                default: // Do nothing
199                }
200                break;
201            case TRK:
202                switch (localName) {
203                case "trkseg":
204                    states.push(currentState);
205                    currentState = State.TRKSEG;
206                    currentTrackSeg = new ArrayList<>();
207                    break;
208                case "link":
209                    states.push(currentState);
210                    currentState = State.LINK;
211                    currentLink = new GpxLink(atts.getValue("href"));
212                    break;
213                case "extensions":
214                    states.push(currentState);
215                    currentState = State.EXT;
216                    currentExtensions = new Extensions();
217                    break;
218                default: // Do nothing
219                }
220                break;
221            case TRKSEG:
222                if ("trkpt".equals(localName)) {
223                    states.push(currentState);
224                    currentState = State.WPT;
225                    currentWayPoint = new WayPoint(parseLatLon(atts));
226                }
227                break;
228            case WPT:
229                switch (localName) {
230                case "link":
231                    states.push(currentState);
232                    currentState = State.LINK;
233                    currentLink = new GpxLink(atts.getValue("href"));
234                    break;
235                case "extensions":
236                    states.push(currentState);
237                    currentState = State.EXT;
238                    currentExtensions = new Extensions();
239                    break;
240                default: // Do nothing
241                }
242                break;
243            case RTE:
244                switch (localName) {
245                case "link":
246                    states.push(currentState);
247                    currentState = State.LINK;
248                    currentLink = new GpxLink(atts.getValue("href"));
249                    break;
250                case "rtept":
251                    states.push(currentState);
252                    currentState = State.WPT;
253                    currentWayPoint = new WayPoint(parseLatLon(atts));
254                    break;
255                case "extensions":
256                    states.push(currentState);
257                    currentState = State.EXT;
258                    currentExtensions = new Extensions();
259                    break;
260                default: // Do nothing
261                }
262                break;
263            default: // Do nothing
264            }
265            accumulator.setLength(0);
266        }
267
268        @Override
269        public void characters(char[] ch, int start, int length) {
270            /**
271             * Remove illegal characters generated by the Nokia Sports Tracker device.
272             * Don't do this crude substitution for all files, since it would destroy
273             * certain unicode characters.
274             */
275            if (nokiaSportsTrackerBug) {
276                for (int i = 0; i < ch.length; ++i) {
277                    if (ch[i] == 1) {
278                        ch[i] = 32;
279                    }
280                }
281                nokiaSportsTrackerBug = false;
282            }
283
284            accumulator.append(ch, start, length);
285        }
286
287        private Map<String, Object> getAttr() {
288            switch (currentState) {
289            case RTE: return currentRoute.attr;
290            case METADATA: return data.attr;
291            case WPT: return currentWayPoint.attr;
292            case TRK: return currentTrackAttr;
293            default: return null;
294            }
295        }
296
297        @SuppressWarnings("unchecked")
298        @Override
299        public void endElement(String namespaceURI, String localName, String qName) {
300            elements.pop();
301            switch (currentState) {
302            case GPX:       // GPX 1.0
303            case METADATA:  // GPX 1.1
304                switch (localName) {
305                case "name":
306                    data.put(META_NAME, accumulator.toString());
307                    break;
308                case "desc":
309                    data.put(META_DESC, accumulator.toString());
310                    break;
311                case "time":
312                    data.put(META_TIME, accumulator.toString());
313                    break;
314                case "keywords":
315                    data.put(META_KEYWORDS, accumulator.toString());
316                    break;
317                case "author":
318                    if ("1.0".equals(version)) {
319                        // author is a string in 1.0, but complex element in 1.1
320                        data.put(META_AUTHOR_NAME, accumulator.toString());
321                    }
322                    break;
323                case "email":
324                    if ("1.0".equals(version)) {
325                        data.put(META_AUTHOR_EMAIL, accumulator.toString());
326                    }
327                    break;
328                case "url":
329                case "urlname":
330                    data.put(localName, accumulator.toString());
331                    break;
332                case "metadata":
333                case "gpx":
334                    if ((currentState == State.METADATA && "metadata".equals(localName)) ||
335                        (currentState == State.GPX && "gpx".equals(localName))) {
336                        convertUrlToLink(data.attr);
337                        if (currentExtensions != null && !currentExtensions.isEmpty()) {
338                            data.put(META_EXTENSIONS, currentExtensions);
339                        }
340                        currentState = states.pop();
341                        break;
342                    }
343                case "bounds":
344                    // do nothing, has been parsed on startElement
345                    break;
346                default:
347                    //TODO: parse extensions
348                }
349                break;
350            case AUTHOR:
351                switch (localName) {
352                case "author":
353                    currentState = states.pop();
354                    break;
355                case "name":
356                    data.put(META_AUTHOR_NAME, accumulator.toString());
357                    break;
358                case "email":
359                    // do nothing, has been parsed on startElement
360                    break;
361                case "link":
362                    data.put(META_AUTHOR_LINK, currentLink);
363                    break;
364                default: // Do nothing
365                }
366                break;
367            case COPYRIGHT:
368                switch (localName) {
369                case "copyright":
370                    currentState = states.pop();
371                    break;
372                case "year":
373                    data.put(META_COPYRIGHT_YEAR, accumulator.toString());
374                    break;
375                case "license":
376                    data.put(META_COPYRIGHT_LICENSE, accumulator.toString());
377                    break;
378                default: // Do nothing
379                }
380                break;
381            case LINK:
382                switch (localName) {
383                case "text":
384                    currentLink.text = accumulator.toString();
385                    break;
386                case "type":
387                    currentLink.type = accumulator.toString();
388                    break;
389                case "link":
390                    if (currentLink.uri == null && accumulator != null && !accumulator.toString().isEmpty()) {
391                        currentLink = new GpxLink(accumulator.toString());
392                    }
393                    currentState = states.pop();
394                    break;
395                default: // Do nothing
396                }
397                if (currentState == State.AUTHOR) {
398                    data.put(META_AUTHOR_LINK, currentLink);
399                } else if (currentState != State.LINK) {
400                    Map<String, Object> attr = getAttr();
401                    if (!attr.containsKey(META_LINKS)) {
402                        attr.put(META_LINKS, new LinkedList<GpxLink>());
403                    }
404                    ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink);
405                }
406                break;
407            case WPT:
408                switch (localName) {
409                case "ele":
410                case "magvar":
411                case "name":
412                case "src":
413                case "geoidheight":
414                case "type":
415                case "sym":
416                case "url":
417                case "urlname":
418                    currentWayPoint.put(localName, accumulator.toString());
419                    break;
420                case "hdop":
421                case "vdop":
422                case "pdop":
423                    try {
424                        currentWayPoint.put(localName, Float.valueOf(accumulator.toString()));
425                    } catch (NumberFormatException e) {
426                        currentWayPoint.put(localName, 0f);
427                    }
428                    break;
429                case "time":
430                case "cmt":
431                case "desc":
432                    currentWayPoint.put(localName, accumulator.toString());
433                    currentWayPoint.setTime();
434                    break;
435                case "rtept":
436                    currentState = states.pop();
437                    convertUrlToLink(currentWayPoint.attr);
438                    currentRoute.routePoints.add(currentWayPoint);
439                    break;
440                case "trkpt":
441                    currentState = states.pop();
442                    convertUrlToLink(currentWayPoint.attr);
443                    currentTrackSeg.add(currentWayPoint);
444                    break;
445                case "wpt":
446                    currentState = states.pop();
447                    convertUrlToLink(currentWayPoint.attr);
448                    if (currentExtensions != null && !currentExtensions.isEmpty()) {
449                        currentWayPoint.put(META_EXTENSIONS, currentExtensions);
450                    }
451                    data.waypoints.add(currentWayPoint);
452                    break;
453                default: // Do nothing
454                }
455                break;
456            case TRKSEG:
457                if ("trkseg".equals(localName)) {
458                    currentState = states.pop();
459                    currentTrack.add(currentTrackSeg);
460                }
461                break;
462            case TRK:
463                switch (localName) {
464                case "trk":
465                    currentState = states.pop();
466                    convertUrlToLink(currentTrackAttr);
467                    data.tracks.add(new ImmutableGpxTrack(currentTrack, currentTrackAttr));
468                    break;
469                case "name":
470                case "cmt":
471                case "desc":
472                case "src":
473                case "type":
474                case "number":
475                case "url":
476                case "urlname":
477                    currentTrackAttr.put(localName, accumulator.toString());
478                    break;
479                default: // Do nothing
480                }
481                break;
482            case EXT:
483                if ("extensions".equals(localName)) {
484                    currentState = states.pop();
485                } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) {
486                    // only interested in extensions written by JOSM
487                    currentExtensions.put(localName, accumulator.toString());
488                }
489                break;
490            default:
491                switch (localName) {
492                case "wpt":
493                    currentState = states.pop();
494                    break;
495                case "rte":
496                    currentState = states.pop();
497                    convertUrlToLink(currentRoute.attr);
498                    data.routes.add(currentRoute);
499                    break;
500                default: // Do nothing
501                }
502            }
503        }
504
505        @Override
506        public void endDocument() throws SAXException  {
507            if (!states.empty())
508                throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
509            Extensions metaExt = (Extensions) data.get(META_EXTENSIONS);
510            if (metaExt != null && "true".equals(metaExt.get("from-server"))) {
511                data.fromServer = true;
512            }
513            gpxData = data;
514        }
515
516        /**
517         * convert url/urlname to link element (GPX 1.0 -&gt; GPX 1.1).
518         * @param attr attributes
519         */
520        private void convertUrlToLink(Map<String, Object> attr) {
521            String url = (String) attr.get("url");
522            String urlname = (String) attr.get("urlname");
523            if (url != null) {
524                if (!attr.containsKey(META_LINKS)) {
525                    attr.put(META_LINKS, new LinkedList<GpxLink>());
526                }
527                GpxLink link = new GpxLink(url);
528                link.text = urlname;
529                @SuppressWarnings("unchecked")
530                Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS);
531                links.add(link);
532            }
533        }
534
535        void tryToFinish() throws SAXException {
536            List<String> remainingElements = new ArrayList<>(elements);
537            for (int i = remainingElements.size() - 1; i >= 0; i--) {
538                endElement(null, remainingElements.get(i), remainingElements.get(i));
539            }
540            endDocument();
541        }
542    }
543
544    /**
545     * Constructs a new {@code GpxReader}, which can later parse the input stream
546     * and store the result in trackData and markerData
547     *
548     * @param source the source input stream
549     * @throws IOException if an IO error occurs, e.g. the input stream is closed.
550     */
551    public GpxReader(InputStream source) throws IOException {
552        Reader utf8stream = UTFInputStreamReader.create(source);
553        Reader filtered = new InvalidXmlCharacterFilter(utf8stream);
554        this.inputSource = new InputSource(filtered);
555    }
556
557    /**
558     * Parse the GPX data.
559     *
560     * @param tryToFinish true, if the reader should return at least part of the GPX
561     * data in case of an error.
562     * @return true if file was properly parsed, false if there was error during
563     * parsing but some data were parsed anyway
564     * @throws SAXException if any SAX parsing error occurs
565     * @throws IOException if any I/O error occurs
566     */
567    public boolean parse(boolean tryToFinish) throws SAXException, IOException {
568        Parser parser = new Parser();
569        try {
570            Utils.parseSafeSAX(inputSource, parser);
571            return true;
572        } catch (SAXException e) {
573            if (tryToFinish) {
574                parser.tryToFinish();
575                if (parser.data.isEmpty())
576                    throw e;
577                String message = e.getMessage();
578                if (e instanceof SAXParseException) {
579                    SAXParseException spe = (SAXParseException) e;
580                    message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber());
581                }
582                Main.warn(message);
583                return false;
584            } else
585                throw e;
586        } catch (ParserConfigurationException e) {
587            Main.error(e); // broken SAXException chaining
588            throw new SAXException(e);
589        }
590    }
591
592    /**
593     * Replies the GPX data.
594     * @return The GPX data
595     */
596    public GpxData getGpxData() {
597        return gpxData;
598    }
599}