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 <ramack@raphael-mack.de> 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}