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

Last change on this file since 13901 was 13901, checked in by Don-vip, 4 months ago

add new XmlUtils class with more "safe factories" methods

  • Property svn:eol-style set to native
File size: 22.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.io.InputStream;
8import java.io.Reader;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.HashMap;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Map;
15import java.util.Stack;
16
17import javax.xml.parsers.ParserConfigurationException;
18
19import org.openstreetmap.josm.data.Bounds;
20import org.openstreetmap.josm.data.coor.LatLon;
21import org.openstreetmap.josm.data.gpx.Extensions;
22import org.openstreetmap.josm.data.gpx.GpxConstants;
23import org.openstreetmap.josm.data.gpx.GpxData;
24import org.openstreetmap.josm.data.gpx.GpxLink;
25import org.openstreetmap.josm.data.gpx.GpxRoute;
26import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
27import org.openstreetmap.josm.data.gpx.WayPoint;
28import org.openstreetmap.josm.tools.Logging;
29import org.openstreetmap.josm.tools.XmlUtils;
30import org.xml.sax.Attributes;
31import org.xml.sax.InputSource;
32import org.xml.sax.SAXException;
33import org.xml.sax.SAXParseException;
34import org.xml.sax.helpers.DefaultHandler;
35
36/**
37 * Read a gpx file.
38 *
39 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br>
40 * Both GPX version 1.0 and 1.1 are supported.
41 *
42 * @author imi, ramack
43 */
44public class GpxReader implements GpxConstants {
45
46    private enum State {
47        INIT,
48        GPX,
49        METADATA,
50        WPT,
51        RTE,
52        TRK,
53        EXT,
54        AUTHOR,
55        LINK,
56        TRKSEG,
57        COPYRIGHT
58    }
59
60    private String version;
61    /** The resulting gpx data */
62    private GpxData gpxData;
63    private final InputSource inputSource;
64
65    private class Parser extends DefaultHandler {
66
67        private GpxData data;
68        private Collection<Collection<WayPoint>> currentTrack;
69        private Map<String, Object> currentTrackAttr;
70        private Collection<WayPoint> currentTrackSeg;
71        private GpxRoute currentRoute;
72        private WayPoint currentWayPoint;
73
74        private State currentState = State.INIT;
75
76        private GpxLink currentLink;
77        private Extensions currentExtensions;
78        private Stack<State> states;
79        private final Stack<String> elements = new Stack<>();
80
81        private StringBuilder accumulator = new StringBuilder();
82
83        private boolean nokiaSportsTrackerBug;
84
85        @Override
86        public void startDocument() {
87            accumulator = new StringBuilder();
88            states = new Stack<>();
89            data = new GpxData();
90        }
91
92        private double parseCoord(Attributes atts, String key) {
93            String val = atts.getValue(key);
94            if (val != null) {
95                return parseCoord(val);
96            } else {
97                // Some software do not respect GPX schema and use "minLat" / "minLon" instead of "minlat" / "minlon"
98                return parseCoord(atts.getValue(key.replaceFirst("l", "L")));
99            }
100        }
101
102        private double parseCoord(String s) {
103            if (s != null) {
104                try {
105                    return Double.parseDouble(s);
106                } catch (NumberFormatException ex) {
107                    Logging.trace(ex);
108                }
109            }
110            return Double.NaN;
111        }
112
113        private LatLon parseLatLon(Attributes atts) {
114            return new LatLon(
115                    parseCoord(atts, "lat"),
116                    parseCoord(atts, "lon"));
117        }
118
119        @Override
120        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
121            elements.push(localName);
122            switch(currentState) {
123            case INIT:
124                states.push(currentState);
125                currentState = State.GPX;
126                data.creator = atts.getValue("creator");
127                version = atts.getValue("version");
128                if (version != null && version.startsWith("1.0")) {
129                    version = "1.0";
130                } else if (!"1.1".equals(version)) {
131                    // unknown version, assume 1.1
132                    version = "1.1";
133                }
134                break;
135            case GPX:
136                switch (localName) {
137                case "metadata":
138                    states.push(currentState);
139                    currentState = State.METADATA;
140                    break;
141                case "wpt":
142                    states.push(currentState);
143                    currentState = State.WPT;
144                    currentWayPoint = new WayPoint(parseLatLon(atts));
145                    break;
146                case "rte":
147                    states.push(currentState);
148                    currentState = State.RTE;
149                    currentRoute = new GpxRoute();
150                    break;
151                case "trk":
152                    states.push(currentState);
153                    currentState = State.TRK;
154                    currentTrack = new ArrayList<>();
155                    currentTrackAttr = new HashMap<>();
156                    break;
157                case "extensions":
158                    states.push(currentState);
159                    currentState = State.EXT;
160                    currentExtensions = new Extensions();
161                    break;
162                case "gpx":
163                    if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) {
164                        nokiaSportsTrackerBug = true;
165                    }
166                    break;
167                default: // Do nothing
168                }
169                break;
170            case METADATA:
171                switch (localName) {
172                case "author":
173                    states.push(currentState);
174                    currentState = State.AUTHOR;
175                    break;
176                case "extensions":
177                    states.push(currentState);
178                    currentState = State.EXT;
179                    currentExtensions = new Extensions();
180                    break;
181                case "copyright":
182                    states.push(currentState);
183                    currentState = State.COPYRIGHT;
184                    data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author"));
185                    break;
186                case "link":
187                    states.push(currentState);
188                    currentState = State.LINK;
189                    currentLink = new GpxLink(atts.getValue("href"));
190                    break;
191                case "bounds":
192                    data.put(META_BOUNDS, new Bounds(
193                                parseCoord(atts, "minlat"),
194                                parseCoord(atts, "minlon"),
195                                parseCoord(atts, "maxlat"),
196                                parseCoord(atts, "maxlon")));
197                    break;
198                default: // Do nothing
199                }
200                break;
201            case AUTHOR:
202                switch (localName) {
203                case "link":
204                    states.push(currentState);
205                    currentState = State.LINK;
206                    currentLink = new GpxLink(atts.getValue("href"));
207                    break;
208                case "email":
209                    data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain"));
210                    break;
211                default: // Do nothing
212                }
213                break;
214            case TRK:
215                switch (localName) {
216                case "trkseg":
217                    states.push(currentState);
218                    currentState = State.TRKSEG;
219                    currentTrackSeg = new ArrayList<>();
220                    break;
221                case "link":
222                    states.push(currentState);
223                    currentState = State.LINK;
224                    currentLink = new GpxLink(atts.getValue("href"));
225                    break;
226                case "extensions":
227                    states.push(currentState);
228                    currentState = State.EXT;
229                    currentExtensions = new Extensions();
230                    break;
231                default: // Do nothing
232                }
233                break;
234            case TRKSEG:
235                if ("trkpt".equals(localName)) {
236                    states.push(currentState);
237                    currentState = State.WPT;
238                    currentWayPoint = new WayPoint(parseLatLon(atts));
239                }
240                break;
241            case WPT:
242                switch (localName) {
243                case "link":
244                    states.push(currentState);
245                    currentState = State.LINK;
246                    currentLink = new GpxLink(atts.getValue("href"));
247                    break;
248                case "extensions":
249                    states.push(currentState);
250                    currentState = State.EXT;
251                    currentExtensions = new Extensions();
252                    break;
253                default: // Do nothing
254                }
255                break;
256            case RTE:
257                switch (localName) {
258                case "link":
259                    states.push(currentState);
260                    currentState = State.LINK;
261                    currentLink = new GpxLink(atts.getValue("href"));
262                    break;
263                case "rtept":
264                    states.push(currentState);
265                    currentState = State.WPT;
266                    currentWayPoint = new WayPoint(parseLatLon(atts));
267                    break;
268                case "extensions":
269                    states.push(currentState);
270                    currentState = State.EXT;
271                    currentExtensions = new Extensions();
272                    break;
273                default: // Do nothing
274                }
275                break;
276            default: // Do nothing
277            }
278            accumulator.setLength(0);
279        }
280
281        @Override
282        public void characters(char[] ch, int start, int length) {
283            /**
284             * Remove illegal characters generated by the Nokia Sports Tracker device.
285             * Don't do this crude substitution for all files, since it would destroy
286             * certain unicode characters.
287             */
288            if (nokiaSportsTrackerBug) {
289                for (int i = 0; i < ch.length; ++i) {
290                    if (ch[i] == 1) {
291                        ch[i] = 32;
292                    }
293                }
294                nokiaSportsTrackerBug = false;
295            }
296
297            accumulator.append(ch, start, length);
298        }
299
300        private Map<String, Object> getAttr() {
301            switch (currentState) {
302            case RTE: return currentRoute.attr;
303            case METADATA: return data.attr;
304            case WPT: return currentWayPoint.attr;
305            case TRK: return currentTrackAttr;
306            default: return null;
307            }
308        }
309
310        @SuppressWarnings("unchecked")
311        @Override
312        public void endElement(String namespaceURI, String localName, String qName) {
313            elements.pop();
314            switch (currentState) {
315            case GPX:       // GPX 1.0
316            case METADATA:  // GPX 1.1
317                switch (localName) {
318                case "name":
319                    data.put(META_NAME, accumulator.toString());
320                    break;
321                case "desc":
322                    data.put(META_DESC, accumulator.toString());
323                    break;
324                case "time":
325                    data.put(META_TIME, accumulator.toString());
326                    break;
327                case "keywords":
328                    data.put(META_KEYWORDS, accumulator.toString());
329                    break;
330                case "author":
331                    if ("1.0".equals(version)) {
332                        // author is a string in 1.0, but complex element in 1.1
333                        data.put(META_AUTHOR_NAME, accumulator.toString());
334                    }
335                    break;
336                case "email":
337                    if ("1.0".equals(version)) {
338                        data.put(META_AUTHOR_EMAIL, accumulator.toString());
339                    }
340                    break;
341                case "url":
342                case "urlname":
343                    data.put(localName, accumulator.toString());
344                    break;
345                case "metadata":
346                case "gpx":
347                    if ((currentState == State.METADATA && "metadata".equals(localName)) ||
348                        (currentState == State.GPX && "gpx".equals(localName))) {
349                        convertUrlToLink(data.attr);
350                        if (currentExtensions != null && !currentExtensions.isEmpty()) {
351                            data.put(META_EXTENSIONS, currentExtensions);
352                        }
353                        currentState = states.pop();
354                    }
355                    break;
356                case "bounds":
357                    // do nothing, has been parsed on startElement
358                    break;
359                default:
360                    //TODO: parse extensions
361                }
362                break;
363            case AUTHOR:
364                switch (localName) {
365                case "author":
366                    currentState = states.pop();
367                    break;
368                case "name":
369                    data.put(META_AUTHOR_NAME, accumulator.toString());
370                    break;
371                case "email":
372                    // do nothing, has been parsed on startElement
373                    break;
374                case "link":
375                    data.put(META_AUTHOR_LINK, currentLink);
376                    break;
377                default: // Do nothing
378                }
379                break;
380            case COPYRIGHT:
381                switch (localName) {
382                case "copyright":
383                    currentState = states.pop();
384                    break;
385                case "year":
386                    data.put(META_COPYRIGHT_YEAR, accumulator.toString());
387                    break;
388                case "license":
389                    data.put(META_COPYRIGHT_LICENSE, accumulator.toString());
390                    break;
391                default: // Do nothing
392                }
393                break;
394            case LINK:
395                switch (localName) {
396                case "text":
397                    currentLink.text = accumulator.toString();
398                    break;
399                case "type":
400                    currentLink.type = accumulator.toString();
401                    break;
402                case "link":
403                    if (currentLink.uri == null && accumulator != null && !accumulator.toString().isEmpty()) {
404                        currentLink = new GpxLink(accumulator.toString());
405                    }
406                    currentState = states.pop();
407                    break;
408                default: // Do nothing
409                }
410                if (currentState == State.AUTHOR) {
411                    data.put(META_AUTHOR_LINK, currentLink);
412                } else if (currentState != State.LINK) {
413                    Map<String, Object> attr = getAttr();
414                    if (attr != null && !attr.containsKey(META_LINKS)) {
415                        attr.put(META_LINKS, new LinkedList<GpxLink>());
416                    }
417                    if (attr != null)
418                        ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink);
419                }
420                break;
421            case WPT:
422                switch (localName) {
423                case "ele":
424                case "magvar":
425                case "name":
426                case "src":
427                case "geoidheight":
428                case "type":
429                case "sym":
430                case "url":
431                case "urlname":
432                    currentWayPoint.put(localName, accumulator.toString());
433                    break;
434                case "hdop":
435                case "vdop":
436                case "pdop":
437                    try {
438                        currentWayPoint.put(localName, Float.valueOf(accumulator.toString()));
439                    } catch (NumberFormatException e) {
440                        currentWayPoint.put(localName, 0f);
441                    }
442                    break;
443                case "time":
444                case "cmt":
445                case "desc":
446                    currentWayPoint.put(localName, accumulator.toString());
447                    currentWayPoint.setTime();
448                    break;
449                case "rtept":
450                    currentState = states.pop();
451                    convertUrlToLink(currentWayPoint.attr);
452                    currentRoute.routePoints.add(currentWayPoint);
453                    break;
454                case "trkpt":
455                    currentState = states.pop();
456                    convertUrlToLink(currentWayPoint.attr);
457                    currentTrackSeg.add(currentWayPoint);
458                    break;
459                case "wpt":
460                    currentState = states.pop();
461                    convertUrlToLink(currentWayPoint.attr);
462                    if (currentExtensions != null && !currentExtensions.isEmpty()) {
463                        currentWayPoint.put(META_EXTENSIONS, currentExtensions);
464                    }
465                    data.waypoints.add(currentWayPoint);
466                    break;
467                default: // Do nothing
468                }
469                break;
470            case TRKSEG:
471                if ("trkseg".equals(localName)) {
472                    currentState = states.pop();
473                    currentTrack.add(currentTrackSeg);
474                }
475                break;
476            case TRK:
477                switch (localName) {
478                case "trk":
479                    currentState = states.pop();
480                    convertUrlToLink(currentTrackAttr);
481                    data.addTrack(new ImmutableGpxTrack(currentTrack, currentTrackAttr));
482                    break;
483                case "name":
484                case "cmt":
485                case "desc":
486                case "src":
487                case "type":
488                case "number":
489                case "url":
490                case "urlname":
491                    currentTrackAttr.put(localName, accumulator.toString());
492                    break;
493                default: // Do nothing
494                }
495                break;
496            case EXT:
497                if ("extensions".equals(localName)) {
498                    currentState = states.pop();
499                } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) {
500                    // only interested in extensions written by JOSM
501                    currentExtensions.put(localName, accumulator.toString());
502                }
503                break;
504            default:
505                switch (localName) {
506                case "wpt":
507                    currentState = states.pop();
508                    break;
509                case "rte":
510                    currentState = states.pop();
511                    convertUrlToLink(currentRoute.attr);
512                    data.addRoute(currentRoute);
513                    break;
514                default: // Do nothing
515                }
516            }
517        }
518
519        @Override
520        public void endDocument() throws SAXException {
521            if (!states.empty())
522                throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
523            Extensions metaExt = (Extensions) data.get(META_EXTENSIONS);
524            if (metaExt != null && "true".equals(metaExt.get("from-server"))) {
525                data.fromServer = true;
526            }
527            gpxData = data;
528        }
529
530        /**
531         * convert url/urlname to link element (GPX 1.0 -&gt; GPX 1.1).
532         * @param attr attributes
533         */
534        private void convertUrlToLink(Map<String, Object> attr) {
535            String url = (String) attr.get("url");
536            String urlname = (String) attr.get("urlname");
537            if (url != null) {
538                if (!attr.containsKey(META_LINKS)) {
539                    attr.put(META_LINKS, new LinkedList<GpxLink>());
540                }
541                GpxLink link = new GpxLink(url);
542                link.text = urlname;
543                @SuppressWarnings("unchecked")
544                Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS);
545                links.add(link);
546            }
547        }
548
549        void tryToFinish() throws SAXException {
550            List<String> remainingElements = new ArrayList<>(elements);
551            for (int i = remainingElements.size() - 1; i >= 0; i--) {
552                endElement(null, remainingElements.get(i), remainingElements.get(i));
553            }
554            endDocument();
555        }
556    }
557
558    /**
559     * Constructs a new {@code GpxReader}, which can later parse the input stream
560     * and store the result in trackData and markerData
561     *
562     * @param source the source input stream
563     * @throws IOException if an IO error occurs, e.g. the input stream is closed.
564     */
565    public GpxReader(InputStream source) throws IOException {
566        Reader utf8stream = UTFInputStreamReader.create(source);
567        Reader filtered = new InvalidXmlCharacterFilter(utf8stream);
568        this.inputSource = new InputSource(filtered);
569    }
570
571    /**
572     * Parse the GPX data.
573     *
574     * @param tryToFinish true, if the reader should return at least part of the GPX
575     * data in case of an error.
576     * @return true if file was properly parsed, false if there was error during
577     * parsing but some data were parsed anyway
578     * @throws SAXException if any SAX parsing error occurs
579     * @throws IOException if any I/O error occurs
580     */
581    public boolean parse(boolean tryToFinish) throws SAXException, IOException {
582        Parser parser = new Parser();
583        try {
584            XmlUtils.parseSafeSAX(inputSource, parser);
585            return true;
586        } catch (SAXException e) {
587            if (tryToFinish) {
588                parser.tryToFinish();
589                if (parser.data.isEmpty())
590                    throw e;
591                String message = e.getMessage();
592                if (e instanceof SAXParseException) {
593                    SAXParseException spe = (SAXParseException) e;
594                    message += ' ' + tr("(at line {0}, column {1})", spe.getLineNumber(), spe.getColumnNumber());
595                }
596                Logging.warn(message);
597                return false;
598            } else
599                throw e;
600        } catch (ParserConfigurationException e) {
601            Logging.error(e); // broken SAXException chaining
602            throw new SAXException(e);
603        }
604    }
605
606    /**
607     * Replies the GPX data.
608     * @return The GPX data
609     */
610    public GpxData getGpxData() {
611        return gpxData;
612    }
613}
Note: See TracBrowser for help on using the repository browser.