001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.gpx;
003
004import java.io.File;
005import java.text.MessageFormat;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.Date;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.Iterator;
014import java.util.List;
015import java.util.LongSummaryStatistics;
016import java.util.Map;
017import java.util.NoSuchElementException;
018import java.util.Objects;
019import java.util.Optional;
020import java.util.Set;
021import java.util.stream.Collectors;
022import java.util.stream.Stream;
023
024import org.openstreetmap.josm.data.Bounds;
025import org.openstreetmap.josm.data.Data;
026import org.openstreetmap.josm.data.DataSource;
027import org.openstreetmap.josm.data.coor.EastNorth;
028import org.openstreetmap.josm.data.gpx.IGpxTrack.GpxTrackChangeListener;
029import org.openstreetmap.josm.data.projection.ProjectionRegistry;
030import org.openstreetmap.josm.gui.MainApplication;
031import org.openstreetmap.josm.gui.layer.GpxLayer;
032import org.openstreetmap.josm.tools.ListenerList;
033import org.openstreetmap.josm.tools.ListeningCollection;
034
035/**
036 * Objects of this class represent a gpx file with tracks, waypoints and routes.
037 * It uses GPX v1.1, see <a href="http://www.topografix.com/GPX/1/1/">the spec</a>
038 * for details.
039 *
040 * @author Raphael Mack &lt;ramack@raphael-mack.de&gt;
041 */
042public class GpxData extends WithAttributes implements Data {
043
044    /**
045     * Constructs a new GpxData.
046     */
047    public GpxData() {}
048
049    /**
050     * Constructs a new GpxData that is currently being initialized, so no listeners will be fired until {@link #endUpdate()} is called.
051     * @param initializing true
052     * @since 15496
053     */
054    public GpxData(boolean initializing) {
055        this.initializing = initializing;
056    }
057
058    /**
059     * The disk file this layer is stored in, if it is a local layer. May be <code>null</code>.
060     */
061    public File storageFile;
062    /**
063     * A boolean flag indicating if the data was read from the OSM server.
064     */
065    public boolean fromServer;
066
067    /**
068     * Creator metadata for this file (usually software)
069     */
070    public String creator;
071
072    /**
073     * A list of tracks this file consists of
074     */
075    private final ArrayList<IGpxTrack> privateTracks = new ArrayList<>();
076    /**
077     * GPX routes in this file
078     */
079    private final ArrayList<GpxRoute> privateRoutes = new ArrayList<>();
080    /**
081     * Addidionaly waypoints for this file.
082     */
083    private final ArrayList<WayPoint> privateWaypoints = new ArrayList<>();
084    /**
085     * All namespaces read from the original file
086     */
087    private final List<XMLNamespace> namespaces = new ArrayList<>();
088    /**
089     * The layer specific prefs formerly saved in the preferences, e.g. drawing options.
090     * NOT the track specific settings (e.g. color, width)
091     */
092    private final Map<String, String> layerPrefs = new HashMap<>();
093
094    private final GpxTrackChangeListener proxy = e -> invalidate();
095    private boolean modified, updating, initializing;
096    private boolean suppressedInvalidate;
097
098    /**
099     * Tracks. Access is discouraged, use {@link #getTracks()} to read.
100     * @see #getTracks()
101     */
102    public final Collection<IGpxTrack> tracks = new ListeningCollection<IGpxTrack>(privateTracks, this::invalidate) {
103
104        @Override
105        protected void removed(IGpxTrack cursor) {
106            cursor.removeListener(proxy);
107            super.removed(cursor);
108        }
109
110        @Override
111        protected void added(IGpxTrack cursor) {
112            super.added(cursor);
113            cursor.addListener(proxy);
114        }
115    };
116
117    /**
118     * Routes. Access is discouraged, use {@link #getTracks()} to read.
119     * @see #getRoutes()
120     */
121    public final Collection<GpxRoute> routes = new ListeningCollection<>(privateRoutes, this::invalidate);
122
123    /**
124     * Waypoints. Access is discouraged, use {@link #getTracks()} to read.
125     * @see #getWaypoints()
126     */
127    public final Collection<WayPoint> waypoints = new ListeningCollection<>(privateWaypoints, this::invalidate);
128
129    /**
130     * All data sources (bounds of downloaded bounds) of this GpxData.<br>
131     * Not part of GPX standard but rather a JOSM extension, needed by the fact that
132     * OSM API does not provide {@code <bounds>} element in its GPX reply.
133     * @since 7575
134     */
135    public final Set<DataSource> dataSources = new HashSet<>();
136
137    private final ListenerList<GpxDataChangeListener> listeners = ListenerList.create();
138
139    private List<GpxTrackSegmentSpan> segSpans;
140
141    /**
142     * Merges data from another object.
143     * @param other existing GPX data
144     */
145    public synchronized void mergeFrom(GpxData other) {
146        mergeFrom(other, false, false);
147    }
148
149    /**
150     * Merges data from another object.
151     * @param other existing GPX data
152     * @param cutOverlapping whether overlapping parts of the given track should be removed
153     * @param connect whether the tracks should be connected on cuts
154     * @since 14338
155     */
156    public synchronized void mergeFrom(GpxData other, boolean cutOverlapping, boolean connect) {
157        if (storageFile == null && other.storageFile != null) {
158            storageFile = other.storageFile;
159        }
160        fromServer = fromServer && other.fromServer;
161
162        for (Map.Entry<String, Object> ent : other.attr.entrySet()) {
163            // TODO: Detect conflicts.
164            String k = ent.getKey();
165            if (META_LINKS.equals(k) && attr.containsKey(META_LINKS)) {
166                Collection<GpxLink> my = super.<GpxLink>getCollection(META_LINKS);
167                @SuppressWarnings("unchecked")
168                Collection<GpxLink> their = (Collection<GpxLink>) ent.getValue();
169                my.addAll(their);
170            } else {
171                put(k, ent.getValue());
172            }
173        }
174
175        if (cutOverlapping) {
176            for (IGpxTrack trk : other.privateTracks) {
177                cutOverlapping(trk, connect);
178            }
179        } else {
180            other.privateTracks.forEach(this::addTrack);
181        }
182        other.privateRoutes.forEach(this::addRoute);
183        other.privateWaypoints.forEach(this::addWaypoint);
184        dataSources.addAll(other.dataSources);
185        invalidate();
186    }
187
188    private void cutOverlapping(IGpxTrack trk, boolean connect) {
189        List<IGpxTrackSegment> segsOld = new ArrayList<>(trk.getSegments());
190        List<IGpxTrackSegment> segsNew = new ArrayList<>();
191        for (IGpxTrackSegment seg : segsOld) {
192            GpxTrackSegmentSpan s = GpxTrackSegmentSpan.tryGetFromSegment(seg);
193            if (s != null && anySegmentOverlapsWith(s)) {
194                List<WayPoint> wpsNew = new ArrayList<>();
195                List<WayPoint> wpsOld = new ArrayList<>(seg.getWayPoints());
196                if (s.isInverted()) {
197                    Collections.reverse(wpsOld);
198                }
199                boolean split = false;
200                WayPoint prevLastOwnWp = null;
201                Date prevWpTime = null;
202                for (WayPoint wp : wpsOld) {
203                    Date wpTime = wp.getDate();
204                    boolean overlap = false;
205                    if (wpTime != null) {
206                        for (GpxTrackSegmentSpan ownspan : getSegmentSpans()) {
207                            if (wpTime.after(ownspan.firstTime) && wpTime.before(ownspan.lastTime)) {
208                                overlap = true;
209                                if (connect) {
210                                    if (!split) {
211                                        wpsNew.add(ownspan.getFirstWp());
212                                    } else {
213                                        connectTracks(prevLastOwnWp, ownspan, trk.getAttributes());
214                                    }
215                                    prevLastOwnWp = ownspan.getLastWp();
216                                }
217                                split = true;
218                                break;
219                            } else if (connect && prevWpTime != null
220                                    && prevWpTime.before(ownspan.firstTime)
221                                    && wpTime.after(ownspan.lastTime)) {
222                                // the overlapping high priority track is shorter than the distance
223                                // between two waypoints of the low priority track
224                                if (split) {
225                                    connectTracks(prevLastOwnWp, ownspan, trk.getAttributes());
226                                    prevLastOwnWp = ownspan.getLastWp();
227                                } else {
228                                    wpsNew.add(ownspan.getFirstWp());
229                                    // splitting needs to be handled here,
230                                    // because other high priority tracks between the same waypoints could follow
231                                    if (!wpsNew.isEmpty()) {
232                                        segsNew.add(new GpxTrackSegment(wpsNew));
233                                    }
234                                    if (!segsNew.isEmpty()) {
235                                        privateTracks.add(new GpxTrack(segsNew, trk.getAttributes()));
236                                    }
237                                    segsNew = new ArrayList<>();
238                                    wpsNew = new ArrayList<>();
239                                    wpsNew.add(ownspan.getLastWp());
240                                    // therefore no break, because another segment could overlap, see above
241                                }
242                            }
243                        }
244                        prevWpTime = wpTime;
245                    }
246                    if (!overlap) {
247                        if (split) {
248                            //track has to be split, because we have an overlapping short track in the middle
249                            if (!wpsNew.isEmpty()) {
250                                segsNew.add(new GpxTrackSegment(wpsNew));
251                            }
252                            if (!segsNew.isEmpty()) {
253                                privateTracks.add(new GpxTrack(segsNew, trk.getAttributes()));
254                            }
255                            segsNew = new ArrayList<>();
256                            wpsNew = new ArrayList<>();
257                            if (connect && prevLastOwnWp != null) {
258                                wpsNew.add(new WayPoint(prevLastOwnWp));
259                            }
260                            prevLastOwnWp = null;
261                            split = false;
262                        }
263                        wpsNew.add(new WayPoint(wp));
264                    }
265                }
266                if (!wpsNew.isEmpty()) {
267                    segsNew.add(new GpxTrackSegment(wpsNew));
268                }
269            } else {
270                segsNew.add(seg);
271            }
272        }
273        if (segsNew.equals(segsOld)) {
274            privateTracks.add(trk);
275        } else if (!segsNew.isEmpty()) {
276            privateTracks.add(new GpxTrack(segsNew, trk.getAttributes()));
277        }
278    }
279
280    private void connectTracks(WayPoint prevWp, GpxTrackSegmentSpan span, Map<String, Object> attr) {
281        if (prevWp != null && !span.lastEquals(prevWp)) {
282            privateTracks.add(new GpxTrack(Arrays.asList(Arrays.asList(new WayPoint(prevWp), span.getFirstWp())), attr));
283        }
284    }
285
286    static class GpxTrackSegmentSpan {
287
288        final Date firstTime;
289        final Date lastTime;
290        private final boolean inv;
291        private final WayPoint firstWp;
292        private final WayPoint lastWp;
293
294        GpxTrackSegmentSpan(WayPoint a, WayPoint b) {
295            Date at = a.getDate();
296            Date bt = b.getDate();
297            inv = bt.before(at);
298            if (inv) {
299                firstWp = b;
300                firstTime = bt;
301                lastWp = a;
302                lastTime = at;
303            } else {
304                firstWp = a;
305                firstTime = at;
306                lastWp = b;
307                lastTime = bt;
308            }
309        }
310
311        WayPoint getFirstWp() {
312            return new WayPoint(firstWp);
313        }
314
315        WayPoint getLastWp() {
316            return new WayPoint(lastWp);
317        }
318
319        // no new instances needed, therefore own methods for that
320
321        boolean firstEquals(Object other) {
322            return firstWp.equals(other);
323        }
324
325        boolean lastEquals(Object other) {
326            return lastWp.equals(other);
327        }
328
329        public boolean isInverted() {
330            return inv;
331        }
332
333        boolean overlapsWith(GpxTrackSegmentSpan other) {
334            return (firstTime.before(other.lastTime) && other.firstTime.before(lastTime))
335                || (other.firstTime.before(lastTime) && firstTime.before(other.lastTime));
336        }
337
338        static GpxTrackSegmentSpan tryGetFromSegment(IGpxTrackSegment seg) {
339            WayPoint b = getNextWpWithTime(seg, true);
340            if (b != null) {
341                WayPoint e = getNextWpWithTime(seg, false);
342                if (e != null) {
343                    return new GpxTrackSegmentSpan(b, e);
344                }
345            }
346            return null;
347        }
348
349        private static WayPoint getNextWpWithTime(IGpxTrackSegment seg, boolean forward) {
350            List<WayPoint> wps = new ArrayList<>(seg.getWayPoints());
351            for (int i = forward ? 0 : wps.size() - 1; i >= 0 && i < wps.size(); i += forward ? 1 : -1) {
352                if (wps.get(i).hasDate()) {
353                    return wps.get(i);
354                }
355            }
356            return null;
357        }
358    }
359
360    /**
361     * Get a list of SegmentSpans containing the beginning and end of each segment
362     * @return the list of SegmentSpans
363     * @since 14338
364     */
365    public synchronized List<GpxTrackSegmentSpan> getSegmentSpans() {
366        if (segSpans == null) {
367            segSpans = new ArrayList<>();
368            for (IGpxTrack trk : privateTracks) {
369                for (IGpxTrackSegment seg : trk.getSegments()) {
370                    GpxTrackSegmentSpan s = GpxTrackSegmentSpan.tryGetFromSegment(seg);
371                    if (s != null) {
372                        segSpans.add(s);
373                    }
374                }
375            }
376            segSpans.sort((o1, o2) -> o1.firstTime.compareTo(o2.firstTime));
377        }
378        return segSpans;
379    }
380
381    private boolean anySegmentOverlapsWith(GpxTrackSegmentSpan other) {
382        for (GpxTrackSegmentSpan s : getSegmentSpans()) {
383            if (s.overlapsWith(other)) {
384                return true;
385            }
386        }
387        return false;
388    }
389
390    /**
391     * Get all tracks contained in this data set.
392     * @return The tracks.
393     */
394    public synchronized Collection<IGpxTrack> getTracks() {
395        return Collections.unmodifiableCollection(privateTracks);
396    }
397
398    /**
399     * Get stream of track segments.
400     * @return {@code Stream<GPXTrack>}
401     */
402    public synchronized Stream<IGpxTrackSegment> getTrackSegmentsStream() {
403        return getTracks().stream().flatMap(trk -> trk.getSegments().stream());
404    }
405
406    /**
407     * Clear all tracks, empties the current privateTracks container,
408     * helper method for some gpx manipulations.
409     */
410    private synchronized void clearTracks() {
411        privateTracks.forEach(t -> t.removeListener(proxy));
412        privateTracks.clear();
413    }
414
415    /**
416     * Add a new track
417     * @param track The new track
418     * @since 12156
419     */
420    public synchronized void addTrack(IGpxTrack track) {
421        if (privateTracks.stream().anyMatch(t -> t == track)) {
422            throw new IllegalArgumentException(MessageFormat.format("The track was already added to this data: {0}", track));
423        }
424        privateTracks.add(track);
425        track.addListener(proxy);
426        invalidate();
427    }
428
429    /**
430     * Remove a track
431     * @param track The old track
432     * @since 12156
433     */
434    public synchronized void removeTrack(IGpxTrack track) {
435        if (!privateTracks.removeIf(t -> t == track)) {
436            throw new IllegalArgumentException(MessageFormat.format("The track was not in this data: {0}", track));
437        }
438        track.removeListener(proxy);
439        invalidate();
440    }
441
442    /**
443     * Combine tracks into a single, segmented track.
444     * The attributes of the first track are used, the rest discarded.
445     *
446     * @since 13210
447     */
448    public synchronized void combineTracksToSegmentedTrack() {
449        List<IGpxTrackSegment> segs = getTrackSegmentsStream()
450                .collect(Collectors.toCollection(ArrayList<IGpxTrackSegment>::new));
451        Map<String, Object> attrs = new HashMap<>(privateTracks.get(0).getAttributes());
452
453        // do not let the name grow if split / combine operations are called iteratively
454        Object name = attrs.get("name");
455        if (name != null) {
456            attrs.put("name", name.toString().replaceFirst(" #\\d+$", ""));
457        }
458
459        clearTracks();
460        addTrack(new GpxTrack(segs, attrs));
461    }
462
463    /**
464     * Ensures a unique name among gpx layers
465     * @param attrs attributes of/for an gpx track, written to if the name appeared previously in {@code counts}.
466     * @param counts a {@code HashMap} of previously seen names, associated with their count.
467     * @param srcLayerName Source layer name
468     * @return the unique name for the gpx track.
469     *
470     * @since 15397
471     */
472    public static String ensureUniqueName(Map<String, Object> attrs, Map<String, Integer> counts, String srcLayerName) {
473        String name = attrs.getOrDefault("name", srcLayerName).toString().replaceFirst(" #\\d+$", "");
474        Integer count = counts.getOrDefault(name, 0) + 1;
475        counts.put(name, count);
476
477        attrs.put("name", MessageFormat.format("{0}{1}", name, " #" + count));
478        return attrs.get("name").toString();
479    }
480
481    /**
482     * Split tracks so that only single-segment tracks remain.
483     * Each segment will make up one individual track after this operation.
484     *
485     * @param srcLayerName Source layer name
486     *
487     * @since 15397
488     */
489    public synchronized void splitTrackSegmentsToTracks(String srcLayerName) {
490        final HashMap<String, Integer> counts = new HashMap<>();
491
492        List<GpxTrack> trks = getTracks().stream()
493            .flatMap(trk -> trk.getSegments().stream().map(seg -> {
494                    HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes());
495                    ensureUniqueName(attrs, counts, srcLayerName);
496                    return new GpxTrack(Arrays.asList(seg), attrs);
497                }))
498            .collect(Collectors.toCollection(ArrayList<GpxTrack>::new));
499
500        clearTracks();
501        trks.stream().forEachOrdered(this::addTrack);
502    }
503
504    /**
505     * Split tracks into layers, the result is one layer for each track.
506     * If this layer currently has only one GpxTrack this is a no-operation.
507     *
508     * The new GpxLayers are added to the LayerManager, the original GpxLayer
509     * is untouched as to preserve potential route or wpt parts.
510     *
511     * @param srcLayerName Source layer name
512     *
513     * @since 15397
514     */
515    public synchronized void splitTracksToLayers(String srcLayerName) {
516        final HashMap<String, Integer> counts = new HashMap<>();
517
518        getTracks().stream()
519            .filter(trk -> privateTracks.size() > 1)
520            .map(trk -> {
521                HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes());
522                GpxData d = new GpxData();
523                d.addTrack(trk);
524                return new GpxLayer(d, ensureUniqueName(attrs, counts, srcLayerName));
525            })
526            .forEachOrdered(layer -> MainApplication.getLayerManager().addLayer(layer));
527    }
528
529    /**
530     * Replies the current number of tracks in this GpxData
531     * @return track count
532     * @since 13210
533     */
534    public synchronized int getTrackCount() {
535        return privateTracks.size();
536    }
537
538    /**
539     * Replies the accumulated total of all track segments,
540     * the sum of segment counts for each track present.
541     * @return track segments count
542     * @since 13210
543     */
544    public synchronized int getTrackSegsCount() {
545        return privateTracks.stream().mapToInt(t -> t.getSegments().size()).sum();
546    }
547
548    /**
549     * Gets the list of all routes defined in this data set.
550     * @return The routes
551     * @since 12156
552     */
553    public synchronized Collection<GpxRoute> getRoutes() {
554        return Collections.unmodifiableCollection(privateRoutes);
555    }
556
557    /**
558     * Add a new route
559     * @param route The new route
560     * @since 12156
561     */
562    public synchronized void addRoute(GpxRoute route) {
563        if (privateRoutes.stream().anyMatch(r -> r == route)) {
564            throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", route));
565        }
566        privateRoutes.add(route);
567        invalidate();
568    }
569
570    /**
571     * Remove a route
572     * @param route The old route
573     * @since 12156
574     */
575    public synchronized void removeRoute(GpxRoute route) {
576        if (!privateRoutes.removeIf(r -> r == route)) {
577            throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", route));
578        }
579        invalidate();
580    }
581
582    /**
583     * Gets a list of all way points in this data set.
584     * @return The way points.
585     * @since 12156
586     */
587    public synchronized Collection<WayPoint> getWaypoints() {
588        return Collections.unmodifiableCollection(privateWaypoints);
589    }
590
591    /**
592     * Add a new waypoint
593     * @param waypoint The new waypoint
594     * @since 12156
595     */
596    public synchronized void addWaypoint(WayPoint waypoint) {
597        if (privateWaypoints.stream().anyMatch(w -> w == waypoint)) {
598            throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", waypoint));
599        }
600        privateWaypoints.add(waypoint);
601        invalidate();
602    }
603
604    /**
605     * Remove a waypoint
606     * @param waypoint The old waypoint
607     * @since 12156
608     */
609    public synchronized void removeWaypoint(WayPoint waypoint) {
610        if (!privateWaypoints.removeIf(w -> w == waypoint)) {
611            throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", waypoint));
612        }
613        invalidate();
614    }
615
616    /**
617     * Determines if this GPX data has one or more track points
618     * @return {@code true} if this GPX data has track points, {@code false} otherwise
619     */
620    public synchronized boolean hasTrackPoints() {
621        return getTrackPoints().findAny().isPresent();
622    }
623
624    /**
625     * Gets a stream of all track points in the segments of the tracks of this data.
626     * @return The stream
627     * @see #getTracks()
628     * @see IGpxTrack#getSegments()
629     * @see IGpxTrackSegment#getWayPoints()
630     * @since 12156
631     */
632    public synchronized Stream<WayPoint> getTrackPoints() {
633        return getTracks().stream().flatMap(trk -> trk.getSegments().stream()).flatMap(trkseg -> trkseg.getWayPoints().stream());
634    }
635
636    /**
637     * Determines if this GPX data has one or more route points
638     * @return {@code true} if this GPX data has route points, {@code false} otherwise
639     */
640    public synchronized boolean hasRoutePoints() {
641        return privateRoutes.stream().anyMatch(rte -> !rte.routePoints.isEmpty());
642    }
643
644    /**
645     * Determines if this GPX data is empty (i.e. does not contain any point)
646     * @return {@code true} if this GPX data is empty, {@code false} otherwise
647     */
648    public synchronized boolean isEmpty() {
649        return !hasRoutePoints() && !hasTrackPoints() && waypoints.isEmpty();
650    }
651
652    /**
653     * Returns the bounds defining the extend of this data, as read in metadata, if any.
654     * If no bounds is defined in metadata, {@code null} is returned. There is no guarantee
655     * that data entirely fit in this bounds, as it is not recalculated. To get recalculated bounds,
656     * see {@link #recalculateBounds()}. To get downloaded areas, see {@link #dataSources}.
657     * @return the bounds defining the extend of this data, or {@code null}.
658     * @see #recalculateBounds()
659     * @see #dataSources
660     * @since 7575
661     */
662    public Bounds getMetaBounds() {
663        Object value = get(META_BOUNDS);
664        if (value instanceof Bounds) {
665            return (Bounds) value;
666        }
667        return null;
668    }
669
670    /**
671     * Calculates the bounding box of available data and returns it.
672     * The bounds are not stored internally, but recalculated every time
673     * this function is called.<br>
674     * To get bounds as read from metadata, see {@link #getMetaBounds()}.<br>
675     * To get downloaded areas, see {@link #dataSources}.<br>
676     *
677     * FIXME might perhaps use visitor pattern?
678     * @return the bounds
679     * @see #getMetaBounds()
680     * @see #dataSources
681     */
682    public synchronized Bounds recalculateBounds() {
683        Bounds bounds = null;
684        for (WayPoint wpt : privateWaypoints) {
685            if (bounds == null) {
686                bounds = new Bounds(wpt.getCoor());
687            } else {
688                bounds.extend(wpt.getCoor());
689            }
690        }
691        for (GpxRoute rte : privateRoutes) {
692            for (WayPoint wpt : rte.routePoints) {
693                if (bounds == null) {
694                    bounds = new Bounds(wpt.getCoor());
695                } else {
696                    bounds.extend(wpt.getCoor());
697                }
698            }
699        }
700        for (IGpxTrack trk : privateTracks) {
701            Bounds trkBounds = trk.getBounds();
702            if (trkBounds != null) {
703                if (bounds == null) {
704                    bounds = new Bounds(trkBounds);
705                } else {
706                    bounds.extend(trkBounds);
707                }
708            }
709        }
710        return bounds;
711    }
712
713    /**
714     * calculates the sum of the lengths of all track segments
715     * @return the length in meters
716     */
717    public synchronized double length() {
718        return privateTracks.stream().mapToDouble(IGpxTrack::length).sum();
719    }
720
721    /**
722     * returns minimum and maximum timestamps in the track
723     * @param trk track to analyze
724     * @return  minimum and maximum dates in array of 2 elements
725     */
726    public static Date[] getMinMaxTimeForTrack(IGpxTrack trk) {
727        final LongSummaryStatistics statistics = trk.getSegments().stream()
728                .flatMap(seg -> seg.getWayPoints().stream())
729                .mapToLong(WayPoint::getTimeInMillis)
730                .summaryStatistics();
731        return statistics.getCount() == 0
732                ? null
733                : new Date[]{new Date(statistics.getMin()), new Date(statistics.getMax())};
734    }
735
736    /**
737    * Returns minimum and maximum timestamps for all tracks
738    * Warning: there are lot of track with broken timestamps,
739    * so we just ignore points from future and from year before 1970 in this method
740    * @return minimum and maximum dates in array of 2 elements
741    * @since 7319
742    */
743    public synchronized Date[] getMinMaxTimeForAllTracks() {
744        long now = System.currentTimeMillis();
745        final LongSummaryStatistics statistics = tracks.stream()
746                .flatMap(trk -> trk.getSegments().stream())
747                .flatMap(seg -> seg.getWayPoints().stream())
748                .mapToLong(WayPoint::getTimeInMillis)
749                .filter(t -> t > 0 && t <= now)
750                .summaryStatistics();
751        return statistics.getCount() == 0
752                ? new Date[0]
753                : new Date[]{new Date(statistics.getMin()), new Date(statistics.getMax())};
754    }
755
756    /**
757     * Makes a WayPoint at the projection of point p onto the track providing p is less than
758     * tolerance away from the track
759     *
760     * @param p : the point to determine the projection for
761     * @param tolerance : must be no further than this from the track
762     * @return the closest point on the track to p, which may be the first or last point if off the
763     * end of a segment, or may be null if nothing close enough
764     */
765    public synchronized WayPoint nearestPointOnTrack(EastNorth p, double tolerance) {
766        /*
767         * assume the coordinates of P are xp,yp, and those of a section of track between two
768         * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
769         *
770         * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
771         *
772         * Also, note that the distance RS^2 is A^2 + B^2
773         *
774         * If RS^2 == 0.0 ignore the degenerate section of track
775         *
776         * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
777         *
778         * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line
779         * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
780         * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
781         *
782         * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
783         *
784         * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
785         *
786         * where RN = sqrt(PR^2 - PN^2)
787         */
788
789        double pnminsq = tolerance * tolerance;
790        EastNorth bestEN = null;
791        double bestTime = Double.NaN;
792        double px = p.east();
793        double py = p.north();
794        double rx = 0.0, ry = 0.0, sx, sy, x, y;
795        for (IGpxTrack track : privateTracks) {
796            for (IGpxTrackSegment seg : track.getSegments()) {
797                WayPoint r = null;
798                for (WayPoint wpSeg : seg.getWayPoints()) {
799                    EastNorth en = wpSeg.getEastNorth(ProjectionRegistry.getProjection());
800                    if (r == null) {
801                        r = wpSeg;
802                        rx = en.east();
803                        ry = en.north();
804                        x = px - rx;
805                        y = py - ry;
806                        double pRsq = x * x + y * y;
807                        if (pRsq < pnminsq) {
808                            pnminsq = pRsq;
809                            bestEN = en;
810                            if (r.hasDate()) {
811                                bestTime = r.getTime();
812                            }
813                        }
814                    } else {
815                        sx = en.east();
816                        sy = en.north();
817                        double a = sy - ry;
818                        double b = rx - sx;
819                        double c = -a * rx - b * ry;
820                        double rssq = a * a + b * b;
821                        if (rssq == 0) {
822                            continue;
823                        }
824                        double pnsq = a * px + b * py + c;
825                        pnsq = pnsq * pnsq / rssq;
826                        if (pnsq < pnminsq) {
827                            x = px - rx;
828                            y = py - ry;
829                            double prsq = x * x + y * y;
830                            x = px - sx;
831                            y = py - sy;
832                            double pssq = x * x + y * y;
833                            if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) {
834                                double rnoverRS = Math.sqrt((prsq - pnsq) / rssq);
835                                double nx = rx - rnoverRS * b;
836                                double ny = ry + rnoverRS * a;
837                                bestEN = new EastNorth(nx, ny);
838                                if (r.hasDate() && wpSeg.hasDate()) {
839                                    bestTime = r.getTime() + rnoverRS * (wpSeg.getTime() - r.getTime());
840                                }
841                                pnminsq = pnsq;
842                            }
843                        }
844                        r = wpSeg;
845                        rx = sx;
846                        ry = sy;
847                    }
848                }
849                if (r != null) {
850                    EastNorth c = r.getEastNorth(ProjectionRegistry.getProjection());
851                    /* if there is only one point in the seg, it will do this twice, but no matter */
852                    rx = c.east();
853                    ry = c.north();
854                    x = px - rx;
855                    y = py - ry;
856                    double prsq = x * x + y * y;
857                    if (prsq < pnminsq) {
858                        pnminsq = prsq;
859                        bestEN = c;
860                        if (r.hasDate()) {
861                            bestTime = r.getTime();
862                        }
863                    }
864                }
865            }
866        }
867        if (bestEN == null)
868            return null;
869        WayPoint best = new WayPoint(ProjectionRegistry.getProjection().eastNorth2latlon(bestEN));
870        if (!Double.isNaN(bestTime)) {
871            best.setTimeInMillis((long) (bestTime * 1000));
872        }
873        return best;
874    }
875
876    /**
877     * Iterate over all track segments and over all routes.
878     *
879     * @param trackVisibility An array indicating which tracks should be
880     * included in the iteration. Can be null, then all tracks are included.
881     * @return an Iterable object, which iterates over all track segments and
882     * over all routes
883     */
884    public Iterable<Line> getLinesIterable(final boolean... trackVisibility) {
885        return () -> new LinesIterator(this, trackVisibility);
886    }
887
888    /**
889     * Resets the internal caches of east/north coordinates.
890     */
891    public synchronized void resetEastNorthCache() {
892        privateWaypoints.forEach(WayPoint::invalidateEastNorthCache);
893        getTrackPoints().forEach(WayPoint::invalidateEastNorthCache);
894        for (GpxRoute route: getRoutes()) {
895            if (route.routePoints == null) {
896                continue;
897            }
898            for (WayPoint wp: route.routePoints) {
899                wp.invalidateEastNorthCache();
900            }
901        }
902    }
903
904    /**
905     * Iterates over all track segments and then over all routes.
906     */
907    public static class LinesIterator implements Iterator<Line> {
908
909        private Iterator<IGpxTrack> itTracks;
910        private int idxTracks;
911        private Iterator<IGpxTrackSegment> itTrackSegments;
912        private final Iterator<GpxRoute> itRoutes;
913
914        private Line next;
915        private final boolean[] trackVisibility;
916        private Map<String, Object> trackAttributes;
917        private IGpxTrack curTrack;
918
919        /**
920         * Constructs a new {@code LinesIterator}.
921         * @param data GPX data
922         * @param trackVisibility An array indicating which tracks should be
923         * included in the iteration. Can be null, then all tracks are included.
924         */
925        public LinesIterator(GpxData data, boolean... trackVisibility) {
926            itTracks = data.tracks.iterator();
927            idxTracks = -1;
928            itRoutes = data.routes.iterator();
929            this.trackVisibility = trackVisibility;
930            next = getNext();
931        }
932
933        @Override
934        public boolean hasNext() {
935            return next != null;
936        }
937
938        @Override
939        public Line next() {
940            if (!hasNext()) {
941                throw new NoSuchElementException();
942            }
943            Line current = next;
944            next = getNext();
945            return current;
946        }
947
948        private Line getNext() {
949            if (itTracks != null) {
950                if (itTrackSegments != null && itTrackSegments.hasNext()) {
951                    return new Line(itTrackSegments.next(), trackAttributes, curTrack.getColor());
952                } else {
953                    while (itTracks.hasNext()) {
954                        curTrack = itTracks.next();
955                        trackAttributes = curTrack.getAttributes();
956                        idxTracks++;
957                        if (trackVisibility != null && !trackVisibility[idxTracks])
958                            continue;
959                        itTrackSegments = curTrack.getSegments().iterator();
960                        if (itTrackSegments.hasNext()) {
961                            return new Line(itTrackSegments.next(), trackAttributes, curTrack.getColor());
962                        }
963                    }
964                    // if we get here, all the Tracks are finished; Continue with Routes
965                    trackAttributes = null;
966                    itTracks = null;
967                }
968            }
969            if (itRoutes.hasNext()) {
970                return new Line(itRoutes.next());
971            }
972            return null;
973        }
974
975        @Override
976        public void remove() {
977            throw new UnsupportedOperationException();
978        }
979    }
980
981    @Override
982    public Collection<DataSource> getDataSources() {
983        return Collections.unmodifiableCollection(dataSources);
984    }
985
986    /**
987     * The layer specific prefs formerly saved in the preferences, e.g. drawing options.
988     * NOT the track specific settings (e.g. color, width)
989     * @return Modifiable map
990     * @since 15496
991     */
992    public Map<String, String> getLayerPrefs() {
993        return layerPrefs;
994    }
995
996    /**
997     * All XML namespaces read from the original file
998     * @return Modifiable list
999     * @since 15496
1000     */
1001    public List<XMLNamespace> getNamespaces() {
1002        return namespaces;
1003    }
1004
1005    @Override
1006    public synchronized int hashCode() {
1007        final int prime = 31;
1008        int result = prime + super.hashCode();
1009        result = prime * result + ((namespaces == null) ? 0 : namespaces.hashCode());
1010        result = prime * result + ((layerPrefs == null) ? 0 : layerPrefs.hashCode());
1011        result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode());
1012        result = prime * result + ((privateRoutes == null) ? 0 : privateRoutes.hashCode());
1013        result = prime * result + ((privateTracks == null) ? 0 : privateTracks.hashCode());
1014        result = prime * result + ((privateWaypoints == null) ? 0 : privateWaypoints.hashCode());
1015        return result;
1016    }
1017
1018    @Override
1019    public synchronized boolean equals(Object obj) {
1020        if (this == obj)
1021            return true;
1022        if (obj == null)
1023            return false;
1024        if (!super.equals(obj))
1025            return false;
1026        if (getClass() != obj.getClass())
1027            return false;
1028        GpxData other = (GpxData) obj;
1029        if (dataSources == null) {
1030            if (other.dataSources != null)
1031                return false;
1032        } else if (!dataSources.equals(other.dataSources))
1033            return false;
1034        if (layerPrefs == null) {
1035            if (other.layerPrefs != null)
1036                return false;
1037        } else if (!layerPrefs.equals(other.layerPrefs))
1038            return false;
1039        if (privateRoutes == null) {
1040            if (other.privateRoutes != null)
1041                return false;
1042        } else if (!privateRoutes.equals(other.privateRoutes))
1043            return false;
1044        if (privateTracks == null) {
1045            if (other.privateTracks != null)
1046                return false;
1047        } else if (!privateTracks.equals(other.privateTracks))
1048            return false;
1049        if (privateWaypoints == null) {
1050            if (other.privateWaypoints != null)
1051                return false;
1052        } else if (!privateWaypoints.equals(other.privateWaypoints))
1053            return false;
1054        if (namespaces == null) {
1055            if (other.namespaces != null)
1056                return false;
1057        } else if (!namespaces.equals(other.namespaces))
1058            return false;
1059        return true;
1060    }
1061
1062    @Override
1063    public void put(String key, Object value) {
1064        super.put(key, value);
1065        invalidate();
1066    }
1067
1068    /**
1069     * Adds a listener that gets called whenever the data changed.
1070     * @param listener The listener
1071     * @since 12156
1072     */
1073    public void addChangeListener(GpxDataChangeListener listener) {
1074        listeners.addListener(listener);
1075    }
1076
1077    /**
1078     * Adds a listener that gets called whenever the data changed. It is added with a weak link
1079     * @param listener The listener
1080     */
1081    public void addWeakChangeListener(GpxDataChangeListener listener) {
1082        listeners.addWeakListener(listener);
1083    }
1084
1085    /**
1086     * Removes a listener that gets called whenever the data changed.
1087     * @param listener The listener
1088     * @since 12156
1089     */
1090    public void removeChangeListener(GpxDataChangeListener listener) {
1091        listeners.removeListener(listener);
1092    }
1093
1094    /**
1095     * Fires event listeners and sets the modified flag to true.
1096     */
1097    public void invalidate() {
1098        fireInvalidate(true);
1099    }
1100
1101    private void fireInvalidate(boolean setModified) {
1102        if (updating || initializing) {
1103            suppressedInvalidate = true;
1104        } else {
1105            if (setModified) {
1106                modified = true;
1107            }
1108            if (listeners.hasListeners()) {
1109                GpxDataChangeEvent e = new GpxDataChangeEvent(this);
1110                listeners.fireEvent(l -> l.gpxDataChanged(e));
1111            }
1112        }
1113    }
1114
1115    /**
1116     * Begins updating this GpxData and prevents listeners from being fired.
1117     * @since 15496
1118     */
1119    public void beginUpdate() {
1120        updating = true;
1121    }
1122
1123    /**
1124     * Finishes updating this GpxData and fires listeners if required.
1125     * @since 15496
1126     */
1127    public void endUpdate() {
1128        boolean setModified = updating;
1129        updating = initializing = false;
1130        if (suppressedInvalidate) {
1131            fireInvalidate(setModified);
1132            suppressedInvalidate = false;
1133        }
1134    }
1135
1136    /**
1137     * A listener that listens to GPX data changes.
1138     * @author Michael Zangl
1139     * @since 12156
1140     */
1141    @FunctionalInterface
1142    public interface GpxDataChangeListener {
1143        /**
1144         * Called when the gpx data changed.
1145         * @param e The event
1146         */
1147        void gpxDataChanged(GpxDataChangeEvent e);
1148    }
1149
1150    /**
1151     * A data change event in any of the gpx data.
1152     * @author Michael Zangl
1153     * @since 12156
1154     */
1155    public static class GpxDataChangeEvent {
1156        private final GpxData source;
1157
1158        GpxDataChangeEvent(GpxData source) {
1159            super();
1160            this.source = source;
1161        }
1162
1163        /**
1164         * Get the data that was changed.
1165         * @return The data.
1166         */
1167        public GpxData getSource() {
1168            return source;
1169        }
1170    }
1171
1172    /**
1173     * @return whether anything has been modified (e.g. colors)
1174     * @since 15496
1175     */
1176    public boolean isModified() {
1177        return modified;
1178    }
1179
1180    /**
1181     * Sets the modified flag to the value.
1182     * @param value modified flag
1183     * @since 15496
1184     */
1185    public void setModified(boolean value) {
1186        modified = value;
1187    }
1188
1189    /**
1190     * A class containing prefix, URI and location of a namespace
1191     * @since 15496
1192     */
1193    public static class XMLNamespace {
1194        private final String uri, prefix;
1195        private String location;
1196
1197        /**
1198         * Creates a schema with prefix and URI, tries to determine prefix from URI
1199         * @param fallbackPrefix the namespace prefix, if not determined from URI
1200         * @param uri the namespace URI
1201         */
1202        public XMLNamespace(String fallbackPrefix, String uri) {
1203            this.prefix = Optional.ofNullable(GpxExtension.findPrefix(uri)).orElse(fallbackPrefix);
1204            this.uri = uri;
1205        }
1206
1207        /**
1208         * Creates a schema with prefix, URI and location.
1209         * Does NOT try to determine prefix from URI!
1210         * @param prefix XML namespace prefix
1211         * @param uri XML namespace URI
1212         * @param location XML namespace location
1213         */
1214        public XMLNamespace(String prefix, String uri, String location) {
1215            this.prefix = prefix;
1216            this.uri = uri;
1217            this.location = location;
1218        }
1219
1220        /**
1221         * @return the URI of the namesapce
1222         */
1223        public String getURI() {
1224            return uri;
1225        }
1226
1227        /**
1228         * @return the prefix of the namespace, determined from URI if possible
1229         */
1230        public String getPrefix() {
1231            return prefix;
1232        }
1233
1234        /**
1235         * @return the location of the schema
1236         */
1237        public String getLocation() {
1238            return location;
1239        }
1240
1241        /**
1242         * Sets the location of the schema
1243         * @param location the location of the schema
1244         */
1245        public void setLocation(String location) {
1246            this.location = location;
1247        }
1248
1249        @Override
1250        public int hashCode() {
1251            return Objects.hash(prefix, uri, location);
1252        }
1253
1254        @Override
1255        public boolean equals(Object obj) {
1256            if (this == obj)
1257                return true;
1258            if (obj == null)
1259                return false;
1260            if (getClass() != obj.getClass())
1261                return false;
1262            XMLNamespace other = (XMLNamespace) obj;
1263            if (prefix == null) {
1264                if (other.prefix != null)
1265                    return false;
1266            } else if (!prefix.equals(other.prefix))
1267                return false;
1268            if (uri == null) {
1269                if (other.uri != null)
1270                    return false;
1271            } else if (!uri.equals(other.uri))
1272                return false;
1273            if (location == null) {
1274                if (other.location != null)
1275                    return false;
1276            } else if (!location.equals(other.location))
1277                return false;
1278            return true;
1279        }
1280    }
1281}