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