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