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<>();
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<>();
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
096        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
097            elements.push(localName);
098            switch(currentState) {
099            case init:
100                states.push(currentState);
101                currentState = State.gpx;
102                data.creator = atts.getValue("creator");
103                version = atts.getValue("version");
104                if (version != null && version.startsWith("1.0")) {
105                    version = "1.0";
106                } else if (!"1.1".equals(version)) {
107                    // unknown version, assume 1.1
108                    version = "1.1";
109                }
110                break;
111            case gpx:
112                switch (localName) {
113                case "metadata":
114                    states.push(currentState);
115                    currentState = State.metadata;
116                    break;
117                case "wpt":
118                    states.push(currentState);
119                    currentState = State.wpt;
120                    currentWayPoint = new WayPoint(parseLatLon(atts));
121                    break;
122                case "rte":
123                    states.push(currentState);
124                    currentState = State.rte;
125                    currentRoute = new GpxRoute();
126                    break;
127                case "trk":
128                    states.push(currentState);
129                    currentState = State.trk;
130                    currentTrack = new ArrayList<>();
131                    currentTrackAttr = new HashMap<>();
132                    break;
133                case "extensions":
134                    states.push(currentState);
135                    currentState = State.ext;
136                    currentExtensions = new Extensions();
137                    break;
138                case "gpx":
139                    if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) {
140                        nokiaSportsTrackerBug = true;
141                    }
142                }
143                break;
144            case metadata:
145                switch (localName) {
146                case "author":
147                    states.push(currentState);
148                    currentState = State.author;
149                    break;
150                case "extensions":
151                    states.push(currentState);
152                    currentState = State.ext;
153                    currentExtensions = new Extensions();
154                    break;
155                case "copyright":
156                    states.push(currentState);
157                    currentState = State.copyright;
158                    data.attr.put(META_COPYRIGHT_AUTHOR, atts.getValue("author"));
159                    break;
160                case "link":
161                    states.push(currentState);
162                    currentState = State.link;
163                    currentLink = new GpxLink(atts.getValue("href"));
164                }
165                break;
166            case author:
167                switch (localName) {
168                case "link":
169                    states.push(currentState);
170                    currentState = State.link;
171                    currentLink = new GpxLink(atts.getValue("href"));
172                    break;
173                case "email":
174                    data.attr.put(META_AUTHOR_EMAIL, atts.getValue("id") + "@" + atts.getValue("domain"));
175                }
176                break;
177            case trk:
178                switch (localName) {
179                case "trkseg":
180                    states.push(currentState);
181                    currentState = State.trkseg;
182                    currentTrackSeg = new ArrayList<>();
183                    break;
184                case "link":
185                    states.push(currentState);
186                    currentState = State.link;
187                    currentLink = new GpxLink(atts.getValue("href"));
188                    break;
189                case "extensions":
190                    states.push(currentState);
191                    currentState = State.ext;
192                    currentExtensions = new Extensions();
193                }
194                break;
195            case trkseg:
196                if ("trkpt".equals(localName)) {
197                    states.push(currentState);
198                    currentState = State.wpt;
199                    currentWayPoint = new WayPoint(parseLatLon(atts));
200                }
201                break;
202            case wpt:
203                switch (localName) {
204                case "link":
205                    states.push(currentState);
206                    currentState = State.link;
207                    currentLink = new GpxLink(atts.getValue("href"));
208                    break;
209                case "extensions":
210                    states.push(currentState);
211                    currentState = State.ext;
212                    currentExtensions = new Extensions();
213                    break;
214                }
215                break;
216            case rte:
217                switch (localName) {
218                case "link":
219                    states.push(currentState);
220                    currentState = State.link;
221                    currentLink = new GpxLink(atts.getValue("href"));
222                    break;
223                case "rtept":
224                    states.push(currentState);
225                    currentState = State.wpt;
226                    currentWayPoint = new WayPoint(parseLatLon(atts));
227                    break;
228                case "extensions":
229                    states.push(currentState);
230                    currentState = State.ext;
231                    currentExtensions = new Extensions();
232                    break;
233                }
234                break;
235            }
236            accumulator.setLength(0);
237        }
238
239        @Override
240        public void characters(char[] ch, int start, int length) {
241            /**
242             * Remove illegal characters generated by the Nokia Sports Tracker device.
243             * Don't do this crude substitution for all files, since it would destroy
244             * certain unicode characters.
245             */
246            if (nokiaSportsTrackerBug) {
247                for (int i=0; i<ch.length; ++i) {
248                    if (ch[i] == 1) {
249                        ch[i] = 32;
250                    }
251                }
252                nokiaSportsTrackerBug = false;
253            }
254
255            accumulator.append(ch, start, length);
256        }
257
258        private Map<String, Object> getAttr() {
259            switch (currentState) {
260            case rte: return currentRoute.attr;
261            case metadata: return data.attr;
262            case wpt: return currentWayPoint.attr;
263            case trk: return currentTrackAttr;
264            default: return null;
265            }
266        }
267
268        @SuppressWarnings("unchecked")
269        @Override
270        public void endElement(String namespaceURI, String localName, String qName) {
271            elements.pop();
272            switch (currentState) {
273            case gpx:       // GPX 1.0
274            case metadata:  // GPX 1.1
275                switch (localName) {
276                case "name":
277                    data.attr.put(META_NAME, accumulator.toString());
278                    break;
279                case "desc":
280                    data.attr.put(META_DESC, accumulator.toString());
281                    break;
282                case "time":
283                    data.attr.put(META_TIME, accumulator.toString());
284                    break;
285                case "keywords":
286                    data.attr.put(META_KEYWORDS, accumulator.toString());
287                    break;
288                case "author":
289                    if ("1.0".equals(version)) {
290                        // author is a string in 1.0, but complex element in 1.1
291                        data.attr.put(META_AUTHOR_NAME, accumulator.toString());
292                    }
293                    break;
294                case "email":
295                    if ("1.0".equals(version)) {
296                        data.attr.put(META_AUTHOR_EMAIL, accumulator.toString());
297                    }
298                    break;
299                case "url":
300                case "urlname":
301                    data.attr.put(localName, accumulator.toString());
302                    break;
303                case "metadata":
304                case "gpx":
305                    if ((currentState == State.metadata && "metadata".equals(localName)) ||
306                        (currentState == State.gpx && "gpx".equals(localName))) {
307                        convertUrlToLink(data.attr);
308                        if (currentExtensions != null && !currentExtensions.isEmpty()) {
309                            data.attr.put(META_EXTENSIONS, currentExtensions);
310                        }
311                        currentState = states.pop();
312                        break;
313                    }
314                default:
315                    //TODO: parse bounds, extensions
316                }
317                break;
318            case author:
319                switch (localName) {
320                case "author":
321                    currentState = states.pop();
322                    break;
323                case "name":
324                    data.attr.put(META_AUTHOR_NAME, accumulator.toString());
325                    break;
326                case "email":
327                    // do nothing, has been parsed on startElement
328                    break;
329                case "link":
330                    data.attr.put(META_AUTHOR_LINK, currentLink);
331                    break;
332                }
333                break;
334            case copyright:
335                switch (localName) {
336                case "copyright":
337                    currentState = states.pop();
338                    break;
339                case "year":
340                    data.attr.put(META_COPYRIGHT_YEAR, accumulator.toString());
341                    break;
342                case "license":
343                    data.attr.put(META_COPYRIGHT_LICENSE, accumulator.toString());
344                    break;
345                }
346                break;
347            case link:
348                switch (localName) {
349                case "text":
350                    currentLink.text = accumulator.toString();
351                    break;
352                case "type":
353                    currentLink.type = accumulator.toString();
354                    break;
355                case "link":
356                    if (currentLink.uri == null && accumulator != null && accumulator.toString().length() != 0) {
357                        currentLink = new GpxLink(accumulator.toString());
358                    }
359                    currentState = states.pop();
360                    break;
361                }
362                if (currentState == State.author) {
363                    data.attr.put(META_AUTHOR_LINK, currentLink);
364                } else if (currentState != State.link) {
365                    Map<String, Object> attr = getAttr();
366                    if (!attr.containsKey(META_LINKS)) {
367                        attr.put(META_LINKS, new LinkedList<GpxLink>());
368                    }
369                    ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink);
370                }
371                break;
372            case wpt:
373                switch (localName) {
374                case "ele":
375                case "magvar":
376                case "name":
377                case "src":
378                case "geoidheight":
379                case "type":
380                case "sym":
381                case "url":
382                case "urlname":
383                    currentWayPoint.attr.put(localName, accumulator.toString());
384                    break;
385                case "hdop":
386                case "vdop":
387                case "pdop":
388                    try {
389                        currentWayPoint.attr.put(localName, Float.parseFloat(accumulator.toString()));
390                    } catch(Exception e) {
391                        currentWayPoint.attr.put(localName, new Float(0));
392                    }
393                    break;
394                case "time":
395                    currentWayPoint.attr.put(localName, accumulator.toString());
396                    currentWayPoint.setTime();
397                    break;
398                case "cmt":
399                case "desc":
400                    currentWayPoint.attr.put(localName, accumulator.toString());
401                    currentWayPoint.setTime();
402                    break;
403                case "rtept":
404                    currentState = states.pop();
405                    convertUrlToLink(currentWayPoint.attr);
406                    currentRoute.routePoints.add(currentWayPoint);
407                    break;
408                case "trkpt":
409                    currentState = states.pop();
410                    convertUrlToLink(currentWayPoint.attr);
411                    currentTrackSeg.add(currentWayPoint);
412                    break;
413                case "wpt":
414                    currentState = states.pop();
415                    convertUrlToLink(currentWayPoint.attr);
416                    if (currentExtensions != null && !currentExtensions.isEmpty()) {
417                        currentWayPoint.attr.put(META_EXTENSIONS, currentExtensions);
418                    }
419                    data.waypoints.add(currentWayPoint);
420                    break;
421                }
422                break;
423            case trkseg:
424                if ("trkseg".equals(localName)) {
425                    currentState = states.pop();
426                    currentTrack.add(currentTrackSeg);
427                }
428                break;
429            case trk:
430                switch (localName) {
431                case "trk":
432                    currentState = states.pop();
433                    convertUrlToLink(currentTrackAttr);
434                    data.tracks.add(new ImmutableGpxTrack(currentTrack, currentTrackAttr));
435                    break;
436                case "name":
437                case "cmt":
438                case "desc":
439                case "src":
440                case "type":
441                case "number":
442                case "url":
443                case "urlname":
444                    currentTrackAttr.put(localName, accumulator.toString());
445                    break;
446                }
447                break;
448            case ext:
449                if ("extensions".equals(localName)) {
450                    currentState = states.pop();
451                } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) {
452                    // only interested in extensions written by JOSM
453                    currentExtensions.put(localName, accumulator.toString());
454                }
455                break;
456            default:
457                switch (localName) {
458                case "wpt":
459                    currentState = states.pop();
460                    break;
461                case "rte":
462                    currentState = states.pop();
463                    convertUrlToLink(currentRoute.attr);
464                    data.routes.add(currentRoute);
465                    break;
466                }
467            }
468        }
469
470        @Override
471        public void endDocument() throws SAXException  {
472            if (!states.empty())
473                throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
474            Extensions metaExt = (Extensions) data.attr.get(META_EXTENSIONS);
475            if (metaExt != null && "true".equals(metaExt.get("from-server"))) {
476                data.fromServer = true;
477            }
478            gpxData = data;
479        }
480
481        /**
482         * convert url/urlname to link element (GPX 1.0 -&gt; GPX 1.1).
483         */
484        private void convertUrlToLink(Map<String, Object> attr) {
485            String url = (String) attr.get("url");
486            String urlname = (String) attr.get("urlname");
487            if (url != null) {
488                if (!attr.containsKey(META_LINKS)) {
489                    attr.put(META_LINKS, new LinkedList<GpxLink>());
490                }
491                GpxLink link = new GpxLink(url);
492                link.text = urlname;
493                @SuppressWarnings({ "unchecked", "rawtypes" })
494                Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS);
495                links.add(link);
496            }
497        }
498
499        public void tryToFinish() throws SAXException {
500            List<String> remainingElements = new ArrayList<>(elements);
501            for (int i=remainingElements.size() - 1; i >= 0; i--) {
502                endElement(null, remainingElements.get(i), remainingElements.get(i));
503            }
504            endDocument();
505        }
506    }
507
508    /**
509     * Constructs a new {@code GpxReader}, which can later parse the input stream 
510     * and store the result in trackData and markerData
511     *
512     * @param source the source input stream
513     * @throws IOException if an IO error occurs, e.g. the input stream is closed.
514     */
515    @SuppressWarnings("resource")
516    public GpxReader(InputStream source) throws IOException {
517        Reader utf8stream = UTFInputStreamReader.create(source);
518        Reader filtered = new InvalidXmlCharacterFilter(utf8stream);
519        this.inputSource = new InputSource(filtered);
520    }
521
522    /**
523     * Parse the GPX data.
524     *
525     * @param tryToFinish true, if the reader should return at least part of the GPX
526     * data in case of an error.
527     * @return true if file was properly parsed, false if there was error during
528     * parsing but some data were parsed anyway
529     * @throws SAXException
530     * @throws IOException
531     */
532    public boolean parse(boolean tryToFinish) throws SAXException, IOException {
533        Parser parser = new Parser();
534        try {
535            SAXParserFactory factory = SAXParserFactory.newInstance();
536            factory.setNamespaceAware(true);
537            factory.newSAXParser().parse(inputSource, parser);
538            return true;
539        } catch (SAXException e) {
540            if (tryToFinish) {
541                parser.tryToFinish();
542                if (parser.data.isEmpty())
543                    throw e;
544                String message = e.getMessage();
545                if (e instanceof SAXParseException) {
546                    SAXParseException spe = ((SAXParseException)e);
547                    message += " " + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber());
548                }
549                Main.warn(message);
550                return false;
551            } else
552                throw e;
553        } catch (ParserConfigurationException e) {
554            Main.error(e); // broken SAXException chaining
555            throw new SAXException(e);
556        }
557    }
558
559    /**
560     * Replies the GPX data.
561     * @return The GPX data
562     */
563    public GpxData getGpxData() {
564        return gpxData;
565    }
566}