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.DoubleSummaryStatistics;
012import java.util.HashMap;
013import java.util.HashSet;
014import java.util.Iterator;
015import java.util.List;
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.Main;
023import org.openstreetmap.josm.data.Bounds;
024import org.openstreetmap.josm.data.Data;
025import org.openstreetmap.josm.data.DataSource;
026import org.openstreetmap.josm.data.coor.EastNorth;
027import org.openstreetmap.josm.data.gpx.GpxTrack.GpxTrackChangeListener;
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     * GXP 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    /**
112     * Merges data from another object.
113     * @param other existing GPX data
114     */
115    public synchronized void mergeFrom(GpxData other) {
116        if (storageFile == null && other.storageFile != null) {
117            storageFile = other.storageFile;
118        }
119        fromServer = fromServer && other.fromServer;
120
121        for (Map.Entry<String, Object> ent : other.attr.entrySet()) {
122            // TODO: Detect conflicts.
123            String k = ent.getKey();
124            if (META_LINKS.equals(k) && attr.containsKey(META_LINKS)) {
125                Collection<GpxLink> my = super.<GpxLink>getCollection(META_LINKS);
126                @SuppressWarnings("unchecked")
127                Collection<GpxLink> their = (Collection<GpxLink>) ent.getValue();
128                my.addAll(their);
129            } else {
130                put(k, ent.getValue());
131            }
132        }
133        other.privateTracks.forEach(this::addTrack);
134        other.privateRoutes.forEach(this::addRoute);
135        other.privateWaypoints.forEach(this::addWaypoint);
136        dataSources.addAll(other.dataSources);
137        fireInvalidate();
138    }
139
140    /**
141     * Get all tracks contained in this data set.
142     * @return The tracks.
143     */
144    public synchronized Collection<GpxTrack> getTracks() {
145        return Collections.unmodifiableCollection(privateTracks);
146    }
147
148    /**
149     * Get stream of track segments.
150     * @return {@code Stream<GPXTrack>}
151     */
152    private synchronized Stream<GpxTrackSegment> getTrackSegmentsStream() {
153        return getTracks().stream().flatMap(trk -> trk.getSegments().stream());
154    }
155
156    /**
157     * Clear all tracks, empties the current privateTracks container,
158     * helper method for some gpx manipulations.
159     */
160    private synchronized void clearTracks() {
161        privateTracks.forEach(t -> t.removeListener(proxy));
162        privateTracks.clear();
163    }
164
165    /**
166     * Add a new track
167     * @param track The new track
168     * @since 12156
169     */
170    public synchronized void addTrack(GpxTrack track) {
171        if (privateTracks.stream().anyMatch(t -> t == track)) {
172            throw new IllegalArgumentException(MessageFormat.format("The track was already added to this data: {0}", track));
173        }
174        privateTracks.add(track);
175        track.addListener(proxy);
176        fireInvalidate();
177    }
178
179    /**
180     * Remove a track
181     * @param track The old track
182     * @since 12156
183     */
184    public synchronized void removeTrack(GpxTrack track) {
185        if (!privateTracks.removeIf(t -> t == track)) {
186            throw new IllegalArgumentException(MessageFormat.format("The track was not in this data: {0}", track));
187        }
188        track.removeListener(proxy);
189        fireInvalidate();
190    }
191
192    /**
193     * Combine tracks into a single, segmented track.
194     * The attributes of the first track are used, the rest discarded.
195     *
196     * @since 13210
197     */
198    public synchronized void combineTracksToSegmentedTrack() {
199        List<GpxTrackSegment> segs = getTrackSegmentsStream()
200                .collect(Collectors.toCollection(ArrayList<GpxTrackSegment>::new));
201        Map<String, Object> attrs = new HashMap<>(privateTracks.get(0).getAttributes());
202
203        // do not let the name grow if split / combine operations are called iteratively
204        attrs.put("name", attrs.get("name").toString().replaceFirst(" #\\d+$", ""));
205
206        clearTracks();
207        addTrack(new ImmutableGpxTrack(segs, attrs));
208    }
209
210    /**
211     * @param attrs attributes of/for an gpx track, written to if the name appeared previously in {@code counts}.
212     * @param counts a {@code HashMap} of previously seen names, associated with their count.
213     * @return the unique name for the gpx track.
214     *
215     * @since 13210
216     */
217    public static String ensureUniqueName(Map<String, Object> attrs, Map<String, Integer> counts) {
218        String name = attrs.getOrDefault("name", "GPX split result").toString();
219        Integer count = counts.getOrDefault(name, 0) + 1;
220        counts.put(name, count);
221
222        attrs.put("name", MessageFormat.format("{0}{1}", name, (count > 1) ? " #"+count : ""));
223        return attrs.get("name").toString();
224    }
225
226    /**
227     * Split tracks so that only single-segment tracks remain.
228     * Each segment will make up one individual track after this operation.
229     *
230     * @since 13210
231     */
232    public synchronized void splitTrackSegmentsToTracks() {
233        final HashMap<String, Integer> counts = new HashMap<>();
234
235        List<GpxTrack> trks = getTracks().stream()
236            .flatMap(trk -> {
237                return trk.getSegments().stream().map(seg -> {
238                    HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes());
239                    ensureUniqueName(attrs, counts);
240                    return new ImmutableGpxTrack(Arrays.asList(seg), attrs);
241                });
242            })
243            .collect(Collectors.toCollection(ArrayList<GpxTrack>::new));
244
245        clearTracks();
246        trks.stream().forEachOrdered(this::addTrack);
247    }
248
249    /**
250     * Split tracks into layers, the result is one layer for each track.
251     * If this layer currently has only one GpxTrack this is a no-operation.
252     *
253     * The new GpxLayers are added to the LayerManager, the original GpxLayer
254     * is untouched as to preserve potential route or wpt parts.
255     *
256     * @since 13210
257     */
258    public synchronized void splitTracksToLayers() {
259        final HashMap<String, Integer> counts = new HashMap<>();
260
261        getTracks().stream()
262            .filter(trk -> privateTracks.size() > 1)
263            .map(trk -> {
264                HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes());
265                GpxData d = new GpxData();
266                d.addTrack(trk);
267                return new GpxLayer(d, ensureUniqueName(attrs, counts)); })
268            .forEachOrdered(layer -> MainApplication.getLayerManager().addLayer(layer));
269    }
270
271    /**
272     * Replies the current number of tracks in this GpxData
273     * @return track count
274     * @since 13210
275     */
276    public synchronized int getTrackCount() {
277        return privateTracks.size();
278    }
279
280    /**
281     * Replies the accumulated total of all track segments,
282     * the sum of segment counts for each track present.
283     * @return track segments count
284     * @since 13210
285     */
286    public synchronized int getTrackSegsCount() {
287        return privateTracks.stream().collect(Collectors.summingInt(t -> t.getSegments().size()));
288    }
289
290    /**
291     * Gets the list of all routes defined in this data set.
292     * @return The routes
293     * @since 12156
294     */
295    public synchronized Collection<GpxRoute> getRoutes() {
296        return Collections.unmodifiableCollection(privateRoutes);
297    }
298
299    /**
300     * Add a new route
301     * @param route The new route
302     * @since 12156
303     */
304    public synchronized void addRoute(GpxRoute route) {
305        if (privateRoutes.stream().anyMatch(r -> r == route)) {
306            throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", route));
307        }
308        privateRoutes.add(route);
309        fireInvalidate();
310    }
311
312    /**
313     * Remove a route
314     * @param route The old route
315     * @since 12156
316     */
317    public synchronized void removeRoute(GpxRoute route) {
318        if (!privateRoutes.removeIf(r -> r == route)) {
319            throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", route));
320        }
321        fireInvalidate();
322    }
323
324    /**
325     * Gets a list of all way points in this data set.
326     * @return The way points.
327     * @since 12156
328     */
329    public synchronized Collection<WayPoint> getWaypoints() {
330        return Collections.unmodifiableCollection(privateWaypoints);
331    }
332
333    /**
334     * Add a new waypoint
335     * @param waypoint The new waypoint
336     * @since 12156
337     */
338    public synchronized void addWaypoint(WayPoint waypoint) {
339        if (privateWaypoints.stream().anyMatch(w -> w == waypoint)) {
340            throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", waypoint));
341        }
342        privateWaypoints.add(waypoint);
343        fireInvalidate();
344    }
345
346    /**
347     * Remove a waypoint
348     * @param waypoint The old waypoint
349     * @since 12156
350     */
351    public synchronized void removeWaypoint(WayPoint waypoint) {
352        if (!privateWaypoints.removeIf(w -> w == waypoint)) {
353            throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", waypoint));
354        }
355        fireInvalidate();
356    }
357
358    /**
359     * Determines if this GPX data has one or more track points
360     * @return {@code true} if this GPX data has track points, {@code false} otherwise
361     */
362    public synchronized boolean hasTrackPoints() {
363        return getTrackPoints().findAny().isPresent();
364    }
365
366    /**
367     * Gets a stream of all track points in the segments of the tracks of this data.
368     * @return The stream
369     * @see #getTracks()
370     * @see GpxTrack#getSegments()
371     * @see GpxTrackSegment#getWayPoints()
372     * @since 12156
373     */
374    public synchronized Stream<WayPoint> getTrackPoints() {
375        return getTracks().stream().flatMap(trk -> trk.getSegments().stream()).flatMap(trkseg -> trkseg.getWayPoints().stream());
376    }
377
378    /**
379     * Determines if this GPX data has one or more route points
380     * @return {@code true} if this GPX data has route points, {@code false} otherwise
381     */
382    public synchronized boolean hasRoutePoints() {
383        return privateRoutes.stream().anyMatch(rte -> !rte.routePoints.isEmpty());
384    }
385
386    /**
387     * Determines if this GPX data is empty (i.e. does not contain any point)
388     * @return {@code true} if this GPX data is empty, {@code false} otherwise
389     */
390    public synchronized boolean isEmpty() {
391        return !hasRoutePoints() && !hasTrackPoints() && waypoints.isEmpty();
392    }
393
394    /**
395     * Returns the bounds defining the extend of this data, as read in metadata, if any.
396     * If no bounds is defined in metadata, {@code null} is returned. There is no guarantee
397     * that data entirely fit in this bounds, as it is not recalculated. To get recalculated bounds,
398     * see {@link #recalculateBounds()}. To get downloaded areas, see {@link #dataSources}.
399     * @return the bounds defining the extend of this data, or {@code null}.
400     * @see #recalculateBounds()
401     * @see #dataSources
402     * @since 7575
403     */
404    public Bounds getMetaBounds() {
405        Object value = get(META_BOUNDS);
406        if (value instanceof Bounds) {
407            return (Bounds) value;
408        }
409        return null;
410    }
411
412    /**
413     * Calculates the bounding box of available data and returns it.
414     * The bounds are not stored internally, but recalculated every time
415     * this function is called.<br>
416     * To get bounds as read from metadata, see {@link #getMetaBounds()}.<br>
417     * To get downloaded areas, see {@link #dataSources}.<br>
418     *
419     * FIXME might perhaps use visitor pattern?
420     * @return the bounds
421     * @see #getMetaBounds()
422     * @see #dataSources
423     */
424    public synchronized Bounds recalculateBounds() {
425        Bounds bounds = null;
426        for (WayPoint wpt : privateWaypoints) {
427            if (bounds == null) {
428                bounds = new Bounds(wpt.getCoor());
429            } else {
430                bounds.extend(wpt.getCoor());
431            }
432        }
433        for (GpxRoute rte : privateRoutes) {
434            for (WayPoint wpt : rte.routePoints) {
435                if (bounds == null) {
436                    bounds = new Bounds(wpt.getCoor());
437                } else {
438                    bounds.extend(wpt.getCoor());
439                }
440            }
441        }
442        for (GpxTrack trk : privateTracks) {
443            Bounds trkBounds = trk.getBounds();
444            if (trkBounds != null) {
445                if (bounds == null) {
446                    bounds = new Bounds(trkBounds);
447                } else {
448                    bounds.extend(trkBounds);
449                }
450            }
451        }
452        return bounds;
453    }
454
455    /**
456     * calculates the sum of the lengths of all track segments
457     * @return the length in meters
458     */
459    public synchronized double length() {
460        return privateTracks.stream().mapToDouble(GpxTrack::length).sum();
461    }
462
463    /**
464     * returns minimum and maximum timestamps in the track
465     * @param trk track to analyze
466     * @return  minimum and maximum dates in array of 2 elements
467     */
468    public static Date[] getMinMaxTimeForTrack(GpxTrack trk) {
469        final DoubleSummaryStatistics statistics = trk.getSegments().stream()
470                .flatMap(seg -> seg.getWayPoints().stream())
471                .mapToDouble(pnt -> pnt.time)
472                .summaryStatistics();
473        return statistics.getCount() == 0
474                ? null
475                : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 1000))};
476    }
477
478    /**
479    * Returns minimum and maximum timestamps for all tracks
480    * Warning: there are lot of track with broken timestamps,
481    * so we just ingore points from future and from year before 1970 in this method
482    * works correctly @since 5815
483     * @return minimum and maximum dates in array of 2 elements
484    */
485    public synchronized Date[] getMinMaxTimeForAllTracks() {
486        double now = System.currentTimeMillis() / 1000.0;
487        final DoubleSummaryStatistics statistics = tracks.stream()
488                .flatMap(trk -> trk.getSegments().stream())
489                .flatMap(seg -> seg.getWayPoints().stream())
490                .mapToDouble(pnt -> pnt.time)
491                .filter(t -> t > 0 && t <= now)
492                .summaryStatistics();
493        return statistics.getCount() == 0
494                ? new Date[0]
495                : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 1000))};
496    }
497
498    /**
499     * Makes a WayPoint at the projection of point p onto the track providing p is less than
500     * tolerance away from the track
501     *
502     * @param p : the point to determine the projection for
503     * @param tolerance : must be no further than this from the track
504     * @return the closest point on the track to p, which may be the first or last point if off the
505     * end of a segment, or may be null if nothing close enough
506     */
507    public synchronized WayPoint nearestPointOnTrack(EastNorth p, double tolerance) {
508        /*
509         * assume the coordinates of P are xp,yp, and those of a section of track between two
510         * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
511         *
512         * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
513         *
514         * Also, note that the distance RS^2 is A^2 + B^2
515         *
516         * If RS^2 == 0.0 ignore the degenerate section of track
517         *
518         * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
519         *
520         * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line
521         * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
522         * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
523         *
524         * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
525         *
526         * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
527         *
528         * where RN = sqrt(PR^2 - PN^2)
529         */
530
531        double pnminsq = tolerance * tolerance;
532        EastNorth bestEN = null;
533        double bestTime = 0.0;
534        double px = p.east();
535        double py = p.north();
536        double rx = 0.0, ry = 0.0, sx, sy, x, y;
537        for (GpxTrack track : privateTracks) {
538            for (GpxTrackSegment seg : track.getSegments()) {
539                WayPoint r = null;
540                for (WayPoint wpSeg : seg.getWayPoints()) {
541                    EastNorth en = wpSeg.getEastNorth(Main.getProjection());
542                    if (r == null) {
543                        r = wpSeg;
544                        rx = en.east();
545                        ry = en.north();
546                        x = px - rx;
547                        y = py - ry;
548                        double pRsq = x * x + y * y;
549                        if (pRsq < pnminsq) {
550                            pnminsq = pRsq;
551                            bestEN = en;
552                            bestTime = r.time;
553                        }
554                    } else {
555                        sx = en.east();
556                        sy = en.north();
557                        double a = sy - ry;
558                        double b = rx - sx;
559                        double c = -a * rx - b * ry;
560                        double rssq = a * a + b * b;
561                        if (rssq == 0) {
562                            continue;
563                        }
564                        double pnsq = a * px + b * py + c;
565                        pnsq = pnsq * pnsq / rssq;
566                        if (pnsq < pnminsq) {
567                            x = px - rx;
568                            y = py - ry;
569                            double prsq = x * x + y * y;
570                            x = px - sx;
571                            y = py - sy;
572                            double pssq = x * x + y * y;
573                            if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) {
574                                double rnoverRS = Math.sqrt((prsq - pnsq) / rssq);
575                                double nx = rx - rnoverRS * b;
576                                double ny = ry + rnoverRS * a;
577                                bestEN = new EastNorth(nx, ny);
578                                bestTime = r.time + rnoverRS * (wpSeg.time - r.time);
579                                pnminsq = pnsq;
580                            }
581                        }
582                        r = wpSeg;
583                        rx = sx;
584                        ry = sy;
585                    }
586                }
587                if (r != null) {
588                    EastNorth c = r.getEastNorth(Main.getProjection());
589                    /* if there is only one point in the seg, it will do this twice, but no matter */
590                    rx = c.east();
591                    ry = c.north();
592                    x = px - rx;
593                    y = py - ry;
594                    double prsq = x * x + y * y;
595                    if (prsq < pnminsq) {
596                        pnminsq = prsq;
597                        bestEN = c;
598                        bestTime = r.time;
599                    }
600                }
601            }
602        }
603        if (bestEN == null)
604            return null;
605        WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN));
606        best.time = bestTime;
607        return best;
608    }
609
610    /**
611     * Iterate over all track segments and over all routes.
612     *
613     * @param trackVisibility An array indicating which tracks should be
614     * included in the iteration. Can be null, then all tracks are included.
615     * @return an Iterable object, which iterates over all track segments and
616     * over all routes
617     */
618    public Iterable<Collection<WayPoint>> getLinesIterable(final boolean... trackVisibility) {
619        return () -> new LinesIterator(this, trackVisibility);
620    }
621
622    /**
623     * Resets the internal caches of east/north coordinates.
624     */
625    public synchronized void resetEastNorthCache() {
626        privateWaypoints.forEach(WayPoint::invalidateEastNorthCache);
627        getTrackPoints().forEach(WayPoint::invalidateEastNorthCache);
628        for (GpxRoute route: getRoutes()) {
629            if (route.routePoints == null) {
630                continue;
631            }
632            for (WayPoint wp: route.routePoints) {
633                wp.invalidateEastNorthCache();
634            }
635        }
636    }
637
638    /**
639     * Iterates over all track segments and then over all routes.
640     */
641    public static class LinesIterator implements Iterator<Collection<WayPoint>> {
642
643        private Iterator<GpxTrack> itTracks;
644        private int idxTracks;
645        private Iterator<GpxTrackSegment> itTrackSegments;
646        private final Iterator<GpxRoute> itRoutes;
647
648        private Collection<WayPoint> next;
649        private final boolean[] trackVisibility;
650
651        /**
652         * Constructs a new {@code LinesIterator}.
653         * @param data GPX data
654         * @param trackVisibility An array indicating which tracks should be
655         * included in the iteration. Can be null, then all tracks are included.
656         */
657        public LinesIterator(GpxData data, boolean... trackVisibility) {
658            itTracks = data.tracks.iterator();
659            idxTracks = -1;
660            itRoutes = data.routes.iterator();
661            this.trackVisibility = trackVisibility;
662            next = getNext();
663        }
664
665        @Override
666        public boolean hasNext() {
667            return next != null;
668        }
669
670        @Override
671        public Collection<WayPoint> next() {
672            if (!hasNext()) {
673                throw new NoSuchElementException();
674            }
675            Collection<WayPoint> current = next;
676            next = getNext();
677            return current;
678        }
679
680        private Collection<WayPoint> getNext() {
681            if (itTracks != null) {
682                if (itTrackSegments != null && itTrackSegments.hasNext()) {
683                    return itTrackSegments.next().getWayPoints();
684                } else {
685                    while (itTracks.hasNext()) {
686                        GpxTrack nxtTrack = itTracks.next();
687                        idxTracks++;
688                        if (trackVisibility != null && !trackVisibility[idxTracks])
689                            continue;
690                        itTrackSegments = nxtTrack.getSegments().iterator();
691                        if (itTrackSegments.hasNext()) {
692                            return itTrackSegments.next().getWayPoints();
693                        }
694                    }
695                    // if we get here, all the Tracks are finished; Continue with Routes
696                    itTracks = null;
697                }
698            }
699            if (itRoutes.hasNext()) {
700                return itRoutes.next().routePoints;
701            }
702            return null;
703        }
704
705        @Override
706        public void remove() {
707            throw new UnsupportedOperationException();
708        }
709    }
710
711    @Override
712    public Collection<DataSource> getDataSources() {
713        return Collections.unmodifiableCollection(dataSources);
714    }
715
716    @Override
717    public synchronized int hashCode() {
718        final int prime = 31;
719        int result = 1;
720        result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode());
721        result = prime * result + ((privateRoutes == null) ? 0 : privateRoutes.hashCode());
722        result = prime * result + ((privateTracks == null) ? 0 : privateTracks.hashCode());
723        result = prime * result + ((privateWaypoints == null) ? 0 : privateWaypoints.hashCode());
724        return result;
725    }
726
727    @Override
728    public synchronized boolean equals(Object obj) {
729        if (this == obj)
730            return true;
731        if (obj == null)
732            return false;
733        if (getClass() != obj.getClass())
734            return false;
735        GpxData other = (GpxData) obj;
736        if (dataSources == null) {
737            if (other.dataSources != null)
738                return false;
739        } else if (!dataSources.equals(other.dataSources))
740            return false;
741        if (privateRoutes == null) {
742            if (other.privateRoutes != null)
743                return false;
744        } else if (!privateRoutes.equals(other.privateRoutes))
745            return false;
746        if (privateTracks == null) {
747            if (other.privateTracks != null)
748                return false;
749        } else if (!privateTracks.equals(other.privateTracks))
750            return false;
751        if (privateWaypoints == null) {
752            if (other.privateWaypoints != null)
753                return false;
754        } else if (!privateWaypoints.equals(other.privateWaypoints))
755            return false;
756        return true;
757    }
758
759    /**
760     * Adds a listener that gets called whenever the data changed.
761     * @param listener The listener
762     * @since 12156
763     */
764    public void addChangeListener(GpxDataChangeListener listener) {
765        listeners.addListener(listener);
766    }
767
768    /**
769     * Adds a listener that gets called whenever the data changed. It is added with a weak link
770     * @param listener The listener
771     */
772    public void addWeakChangeListener(GpxDataChangeListener listener) {
773        listeners.addWeakListener(listener);
774    }
775
776    /**
777     * Removes a listener that gets called whenever the data changed.
778     * @param listener The listener
779     * @since 12156
780     */
781    public void removeChangeListener(GpxDataChangeListener listener) {
782        listeners.removeListener(listener);
783    }
784
785    private void fireInvalidate() {
786        if (listeners.hasListeners()) {
787            GpxDataChangeEvent e = new GpxDataChangeEvent(this);
788            listeners.fireEvent(l -> l.gpxDataChanged(e));
789        }
790    }
791
792    /**
793     * A listener that listens to GPX data changes.
794     * @author Michael Zangl
795     * @since 12156
796     */
797    @FunctionalInterface
798    public interface GpxDataChangeListener {
799        /**
800         * Called when the gpx data changed.
801         * @param e The event
802         */
803        void gpxDataChanged(GpxDataChangeEvent e);
804    }
805
806    /**
807     * A data change event in any of the gpx data.
808     * @author Michael Zangl
809     * @since 12156
810     */
811    public static class GpxDataChangeEvent {
812        private final GpxData source;
813
814        GpxDataChangeEvent(GpxData source) {
815            super();
816            this.source = source;
817        }
818
819        /**
820         * Get the data that was changed.
821         * @return The data.
822         */
823        public GpxData getSource() {
824            return source;
825        }
826    }
827}