source: josm/trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java@ 13536

Last change on this file since 13536 was 13257, checked in by Don-vip, 6 years ago

fix some recent Sonar issues

  • Property svn:eol-style set to native
File size: 29.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.gpx;
3
4import java.io.File;
5import java.text.MessageFormat;
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.Date;
11import java.util.DoubleSummaryStatistics;
12import java.util.HashMap;
13import java.util.HashSet;
14import java.util.Iterator;
15import java.util.List;
16import java.util.Map;
17import java.util.NoSuchElementException;
18import java.util.Set;
19import java.util.stream.Collectors;
20import java.util.stream.Stream;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.data.Bounds;
24import org.openstreetmap.josm.data.Data;
25import org.openstreetmap.josm.data.DataSource;
26import org.openstreetmap.josm.data.coor.EastNorth;
27import org.openstreetmap.josm.data.gpx.GpxTrack.GpxTrackChangeListener;
28import org.openstreetmap.josm.gui.MainApplication;
29import org.openstreetmap.josm.gui.layer.GpxLayer;
30import org.openstreetmap.josm.tools.ListenerList;
31import org.openstreetmap.josm.tools.ListeningCollection;
32
33/**
34 * Objects of this class represent a gpx file with tracks, waypoints and routes.
35 * It uses GPX v1.1, see <a href="http://www.topografix.com/GPX/1/1/">the spec</a>
36 * for details.
37 *
38 * @author Raphael Mack &lt;ramack@raphael-mack.de&gt;
39 */
40public class GpxData extends WithAttributes implements Data {
41
42 /**
43 * The disk file this layer is stored in, if it is a local layer. May be <code>null</code>.
44 */
45 public File storageFile;
46 /**
47 * A boolean flag indicating if the data was read from the OSM server.
48 */
49 public boolean fromServer;
50
51 /**
52 * Creator metadata for this file (usually software)
53 */
54 public String creator;
55
56 /**
57 * A list of tracks this file consists of
58 */
59 private final ArrayList<GpxTrack> privateTracks = new ArrayList<>();
60 /**
61 * GXP routes in this file
62 */
63 private final ArrayList<GpxRoute> privateRoutes = new ArrayList<>();
64 /**
65 * Addidionaly waypoints for this file.
66 */
67 private final ArrayList<WayPoint> privateWaypoints = new ArrayList<>();
68 private final GpxTrackChangeListener proxy = e -> fireInvalidate();
69
70 /**
71 * Tracks. Access is discouraged, use {@link #getTracks()} to read.
72 * @see #getTracks()
73 */
74 public final Collection<GpxTrack> tracks = new ListeningCollection<GpxTrack>(privateTracks, this::fireInvalidate) {
75
76 @Override
77 protected void removed(GpxTrack cursor) {
78 cursor.removeListener(proxy);
79 super.removed(cursor);
80 }
81
82 @Override
83 protected void added(GpxTrack cursor) {
84 super.added(cursor);
85 cursor.addListener(proxy);
86 }
87 };
88
89 /**
90 * Routes. Access is discouraged, use {@link #getTracks()} to read.
91 * @see #getRoutes()
92 */
93 public final Collection<GpxRoute> routes = new ListeningCollection<>(privateRoutes, this::fireInvalidate);
94
95 /**
96 * Waypoints. Access is discouraged, use {@link #getTracks()} to read.
97 * @see #getWaypoints()
98 */
99 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}
Note: See TracBrowser for help on using the repository browser.