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