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 Object name = attrs.get("name"); 429 if (name != null) { 430 attrs.put("name", name.toString().replaceFirst(" #\\d+$", "")); 431 } 432 433 clearTracks(); 434 addTrack(new ImmutableGpxTrack(segs, attrs)); 435 } 436 437 /** 438 * @param attrs attributes of/for an gpx track, written to if the name appeared previously in {@code counts}. 439 * @param counts a {@code HashMap} of previously seen names, associated with their count. 440 * @return the unique name for the gpx track. 441 * 442 * @since 13210 443 */ 444 public static String ensureUniqueName(Map<String, Object> attrs, Map<String, Integer> counts) { 445 String name = attrs.getOrDefault("name", "GPX split result").toString(); 446 Integer count = counts.getOrDefault(name, 0) + 1; 447 counts.put(name, count); 448 449 attrs.put("name", MessageFormat.format("{0}{1}", name, (count > 1) ? " #"+count : "")); 450 return attrs.get("name").toString(); 451 } 452 453 /** 454 * Split tracks so that only single-segment tracks remain. 455 * Each segment will make up one individual track after this operation. 456 * 457 * @since 13210 458 */ 459 public synchronized void splitTrackSegmentsToTracks() { 460 final HashMap<String, Integer> counts = new HashMap<>(); 461 462 List<GpxTrack> trks = getTracks().stream() 463 .flatMap(trk -> { 464 return trk.getSegments().stream().map(seg -> { 465 HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes()); 466 ensureUniqueName(attrs, counts); 467 return new ImmutableGpxTrack(Arrays.asList(seg), attrs); 468 }); 469 }) 470 .collect(Collectors.toCollection(ArrayList<GpxTrack>::new)); 471 472 clearTracks(); 473 trks.stream().forEachOrdered(this::addTrack); 474 } 475 476 /** 477 * Split tracks into layers, the result is one layer for each track. 478 * If this layer currently has only one GpxTrack this is a no-operation. 479 * 480 * The new GpxLayers are added to the LayerManager, the original GpxLayer 481 * is untouched as to preserve potential route or wpt parts. 482 * 483 * @since 13210 484 */ 485 public synchronized void splitTracksToLayers() { 486 final HashMap<String, Integer> counts = new HashMap<>(); 487 488 getTracks().stream() 489 .filter(trk -> privateTracks.size() > 1) 490 .map(trk -> { 491 HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes()); 492 GpxData d = new GpxData(); 493 d.addTrack(trk); 494 return new GpxLayer(d, ensureUniqueName(attrs, counts)); 495 }) 496 .forEachOrdered(layer -> MainApplication.getLayerManager().addLayer(layer)); 497 } 498 499 /** 500 * Replies the current number of tracks in this GpxData 501 * @return track count 502 * @since 13210 503 */ 504 public synchronized int getTrackCount() { 505 return privateTracks.size(); 506 } 507 508 /** 509 * Replies the accumulated total of all track segments, 510 * the sum of segment counts for each track present. 511 * @return track segments count 512 * @since 13210 513 */ 514 public synchronized int getTrackSegsCount() { 515 return privateTracks.stream().mapToInt(t -> t.getSegments().size()).sum(); 516 } 517 518 /** 519 * Gets the list of all routes defined in this data set. 520 * @return The routes 521 * @since 12156 522 */ 523 public synchronized Collection<GpxRoute> getRoutes() { 524 return Collections.unmodifiableCollection(privateRoutes); 525 } 526 527 /** 528 * Add a new route 529 * @param route The new route 530 * @since 12156 531 */ 532 public synchronized void addRoute(GpxRoute route) { 533 if (privateRoutes.stream().anyMatch(r -> r == route)) { 534 throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", route)); 535 } 536 privateRoutes.add(route); 537 fireInvalidate(); 538 } 539 540 /** 541 * Remove a route 542 * @param route The old route 543 * @since 12156 544 */ 545 public synchronized void removeRoute(GpxRoute route) { 546 if (!privateRoutes.removeIf(r -> r == route)) { 547 throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", route)); 548 } 549 fireInvalidate(); 550 } 551 552 /** 553 * Gets a list of all way points in this data set. 554 * @return The way points. 555 * @since 12156 556 */ 557 public synchronized Collection<WayPoint> getWaypoints() { 558 return Collections.unmodifiableCollection(privateWaypoints); 559 } 560 561 /** 562 * Add a new waypoint 563 * @param waypoint The new waypoint 564 * @since 12156 565 */ 566 public synchronized void addWaypoint(WayPoint waypoint) { 567 if (privateWaypoints.stream().anyMatch(w -> w == waypoint)) { 568 throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", waypoint)); 569 } 570 privateWaypoints.add(waypoint); 571 fireInvalidate(); 572 } 573 574 /** 575 * Remove a waypoint 576 * @param waypoint The old waypoint 577 * @since 12156 578 */ 579 public synchronized void removeWaypoint(WayPoint waypoint) { 580 if (!privateWaypoints.removeIf(w -> w == waypoint)) { 581 throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", waypoint)); 582 } 583 fireInvalidate(); 584 } 585 586 /** 587 * Determines if this GPX data has one or more track points 588 * @return {@code true} if this GPX data has track points, {@code false} otherwise 589 */ 590 public synchronized boolean hasTrackPoints() { 591 return getTrackPoints().findAny().isPresent(); 592 } 593 594 /** 595 * Gets a stream of all track points in the segments of the tracks of this data. 596 * @return The stream 597 * @see #getTracks() 598 * @see GpxTrack#getSegments() 599 * @see GpxTrackSegment#getWayPoints() 600 * @since 12156 601 */ 602 public synchronized Stream<WayPoint> getTrackPoints() { 603 return getTracks().stream().flatMap(trk -> trk.getSegments().stream()).flatMap(trkseg -> trkseg.getWayPoints().stream()); 604 } 605 606 /** 607 * Determines if this GPX data has one or more route points 608 * @return {@code true} if this GPX data has route points, {@code false} otherwise 609 */ 610 public synchronized boolean hasRoutePoints() { 611 return privateRoutes.stream().anyMatch(rte -> !rte.routePoints.isEmpty()); 612 } 613 614 /** 615 * Determines if this GPX data is empty (i.e. does not contain any point) 616 * @return {@code true} if this GPX data is empty, {@code false} otherwise 617 */ 618 public synchronized boolean isEmpty() { 619 return !hasRoutePoints() && !hasTrackPoints() && waypoints.isEmpty(); 620 } 621 622 /** 623 * Returns the bounds defining the extend of this data, as read in metadata, if any. 624 * If no bounds is defined in metadata, {@code null} is returned. There is no guarantee 625 * that data entirely fit in this bounds, as it is not recalculated. To get recalculated bounds, 626 * see {@link #recalculateBounds()}. To get downloaded areas, see {@link #dataSources}. 627 * @return the bounds defining the extend of this data, or {@code null}. 628 * @see #recalculateBounds() 629 * @see #dataSources 630 * @since 7575 631 */ 632 public Bounds getMetaBounds() { 633 Object value = get(META_BOUNDS); 634 if (value instanceof Bounds) { 635 return (Bounds) value; 636 } 637 return null; 638 } 639 640 /** 641 * Calculates the bounding box of available data and returns it. 642 * The bounds are not stored internally, but recalculated every time 643 * this function is called.<br> 644 * To get bounds as read from metadata, see {@link #getMetaBounds()}.<br> 645 * To get downloaded areas, see {@link #dataSources}.<br> 646 * 647 * FIXME might perhaps use visitor pattern? 648 * @return the bounds 649 * @see #getMetaBounds() 650 * @see #dataSources 651 */ 652 public synchronized Bounds recalculateBounds() { 653 Bounds bounds = null; 654 for (WayPoint wpt : privateWaypoints) { 655 if (bounds == null) { 656 bounds = new Bounds(wpt.getCoor()); 657 } else { 658 bounds.extend(wpt.getCoor()); 659 } 660 } 661 for (GpxRoute rte : privateRoutes) { 662 for (WayPoint wpt : rte.routePoints) { 663 if (bounds == null) { 664 bounds = new Bounds(wpt.getCoor()); 665 } else { 666 bounds.extend(wpt.getCoor()); 667 } 668 } 669 } 670 for (GpxTrack trk : privateTracks) { 671 Bounds trkBounds = trk.getBounds(); 672 if (trkBounds != null) { 673 if (bounds == null) { 674 bounds = new Bounds(trkBounds); 675 } else { 676 bounds.extend(trkBounds); 677 } 678 } 679 } 680 return bounds; 681 } 682 683 /** 684 * calculates the sum of the lengths of all track segments 685 * @return the length in meters 686 */ 687 public synchronized double length() { 688 return privateTracks.stream().mapToDouble(GpxTrack::length).sum(); 689 } 690 691 /** 692 * returns minimum and maximum timestamps in the track 693 * @param trk track to analyze 694 * @return minimum and maximum dates in array of 2 elements 695 */ 696 public static Date[] getMinMaxTimeForTrack(GpxTrack trk) { 697 final LongSummaryStatistics statistics = trk.getSegments().stream() 698 .flatMap(seg -> seg.getWayPoints().stream()) 699 .mapToLong(WayPoint::getTimeInMillis) 700 .summaryStatistics(); 701 return statistics.getCount() == 0 702 ? null 703 : new Date[]{new Date(statistics.getMin()), new Date(statistics.getMax())}; 704 } 705 706 /** 707 * Returns minimum and maximum timestamps for all tracks 708 * Warning: there are lot of track with broken timestamps, 709 * so we just ingore points from future and from year before 1970 in this method 710 * works correctly @since 5815 711 * @return minimum and maximum dates in array of 2 elements 712 */ 713 public synchronized Date[] getMinMaxTimeForAllTracks() { 714 long now = System.currentTimeMillis(); 715 final LongSummaryStatistics statistics = tracks.stream() 716 .flatMap(trk -> trk.getSegments().stream()) 717 .flatMap(seg -> seg.getWayPoints().stream()) 718 .mapToLong(WayPoint::getTimeInMillis) 719 .filter(t -> t > 0 && t <= now) 720 .summaryStatistics(); 721 return statistics.getCount() == 0 722 ? new Date[0] 723 : new Date[]{new Date(statistics.getMin()), new Date(statistics.getMax())}; 724 } 725 726 /** 727 * Makes a WayPoint at the projection of point p onto the track providing p is less than 728 * tolerance away from the track 729 * 730 * @param p : the point to determine the projection for 731 * @param tolerance : must be no further than this from the track 732 * @return the closest point on the track to p, which may be the first or last point if off the 733 * end of a segment, or may be null if nothing close enough 734 */ 735 public synchronized WayPoint nearestPointOnTrack(EastNorth p, double tolerance) { 736 /* 737 * assume the coordinates of P are xp,yp, and those of a section of track between two 738 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point. 739 * 740 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr 741 * 742 * Also, note that the distance RS^2 is A^2 + B^2 743 * 744 * If RS^2 == 0.0 ignore the degenerate section of track 745 * 746 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line 747 * 748 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line 749 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 - 750 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2 751 * 752 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2 753 * 754 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A 755 * 756 * where RN = sqrt(PR^2 - PN^2) 757 */ 758 759 double pnminsq = tolerance * tolerance; 760 EastNorth bestEN = null; 761 double bestTime = Double.NaN; 762 double px = p.east(); 763 double py = p.north(); 764 double rx = 0.0, ry = 0.0, sx, sy, x, y; 765 for (GpxTrack track : privateTracks) { 766 for (GpxTrackSegment seg : track.getSegments()) { 767 WayPoint r = null; 768 for (WayPoint wpSeg : seg.getWayPoints()) { 769 EastNorth en = wpSeg.getEastNorth(ProjectionRegistry.getProjection()); 770 if (r == null) { 771 r = wpSeg; 772 rx = en.east(); 773 ry = en.north(); 774 x = px - rx; 775 y = py - ry; 776 double pRsq = x * x + y * y; 777 if (pRsq < pnminsq) { 778 pnminsq = pRsq; 779 bestEN = en; 780 if (r.hasDate()) { 781 bestTime = r.getTime(); 782 } 783 } 784 } else { 785 sx = en.east(); 786 sy = en.north(); 787 double a = sy - ry; 788 double b = rx - sx; 789 double c = -a * rx - b * ry; 790 double rssq = a * a + b * b; 791 if (rssq == 0) { 792 continue; 793 } 794 double pnsq = a * px + b * py + c; 795 pnsq = pnsq * pnsq / rssq; 796 if (pnsq < pnminsq) { 797 x = px - rx; 798 y = py - ry; 799 double prsq = x * x + y * y; 800 x = px - sx; 801 y = py - sy; 802 double pssq = x * x + y * y; 803 if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) { 804 double rnoverRS = Math.sqrt((prsq - pnsq) / rssq); 805 double nx = rx - rnoverRS * b; 806 double ny = ry + rnoverRS * a; 807 bestEN = new EastNorth(nx, ny); 808 if (r.hasDate() && wpSeg.hasDate()) { 809 bestTime = r.getTime() + rnoverRS * (wpSeg.getTime() - r.getTime()); 810 } 811 pnminsq = pnsq; 812 } 813 } 814 r = wpSeg; 815 rx = sx; 816 ry = sy; 817 } 818 } 819 if (r != null) { 820 EastNorth c = r.getEastNorth(ProjectionRegistry.getProjection()); 821 /* if there is only one point in the seg, it will do this twice, but no matter */ 822 rx = c.east(); 823 ry = c.north(); 824 x = px - rx; 825 y = py - ry; 826 double prsq = x * x + y * y; 827 if (prsq < pnminsq) { 828 pnminsq = prsq; 829 bestEN = c; 830 if (r.hasDate()) { 831 bestTime = r.getTime(); 832 } 833 } 834 } 835 } 836 } 837 if (bestEN == null) 838 return null; 839 WayPoint best = new WayPoint(ProjectionRegistry.getProjection().eastNorth2latlon(bestEN)); 840 if (!Double.isNaN(bestTime)) { 841 best.setTimeInMillis((long) (bestTime * 1000)); 842 } 843 return best; 844 } 845 846 /** 847 * Iterate over all track segments and over all routes. 848 * 849 * @param trackVisibility An array indicating which tracks should be 850 * included in the iteration. Can be null, then all tracks are included. 851 * @return an Iterable object, which iterates over all track segments and 852 * over all routes 853 */ 854 public Iterable<Line> getLinesIterable(final boolean... trackVisibility) { 855 return () -> new LinesIterator(this, trackVisibility); 856 } 857 858 /** 859 * Resets the internal caches of east/north coordinates. 860 */ 861 public synchronized void resetEastNorthCache() { 862 privateWaypoints.forEach(WayPoint::invalidateEastNorthCache); 863 getTrackPoints().forEach(WayPoint::invalidateEastNorthCache); 864 for (GpxRoute route: getRoutes()) { 865 if (route.routePoints == null) { 866 continue; 867 } 868 for (WayPoint wp: route.routePoints) { 869 wp.invalidateEastNorthCache(); 870 } 871 } 872 } 873 874 /** 875 * Iterates over all track segments and then over all routes. 876 */ 877 public static class LinesIterator implements Iterator<Line> { 878 879 private Iterator<GpxTrack> itTracks; 880 private int idxTracks; 881 private Iterator<GpxTrackSegment> itTrackSegments; 882 private final Iterator<GpxRoute> itRoutes; 883 884 private Line next; 885 private final boolean[] trackVisibility; 886 private Map<String, Object> trackAttributes; 887 888 /** 889 * Constructs a new {@code LinesIterator}. 890 * @param data GPX data 891 * @param trackVisibility An array indicating which tracks should be 892 * included in the iteration. Can be null, then all tracks are included. 893 */ 894 public LinesIterator(GpxData data, boolean... trackVisibility) { 895 itTracks = data.tracks.iterator(); 896 idxTracks = -1; 897 itRoutes = data.routes.iterator(); 898 this.trackVisibility = trackVisibility; 899 next = getNext(); 900 } 901 902 @Override 903 public boolean hasNext() { 904 return next != null; 905 } 906 907 @Override 908 public Line next() { 909 if (!hasNext()) { 910 throw new NoSuchElementException(); 911 } 912 Line current = next; 913 next = getNext(); 914 return current; 915 } 916 917 private Line getNext() { 918 if (itTracks != null) { 919 if (itTrackSegments != null && itTrackSegments.hasNext()) { 920 return new Line(itTrackSegments.next(), trackAttributes); 921 } else { 922 while (itTracks.hasNext()) { 923 GpxTrack nxtTrack = itTracks.next(); 924 trackAttributes = nxtTrack.getAttributes(); 925 idxTracks++; 926 if (trackVisibility != null && !trackVisibility[idxTracks]) 927 continue; 928 itTrackSegments = nxtTrack.getSegments().iterator(); 929 if (itTrackSegments.hasNext()) { 930 return new Line(itTrackSegments.next(), trackAttributes); 931 } 932 } 933 // if we get here, all the Tracks are finished; Continue with Routes 934 trackAttributes = null; 935 itTracks = null; 936 } 937 } 938 if (itRoutes.hasNext()) { 939 return new Line(itRoutes.next()); 940 } 941 return null; 942 } 943 944 @Override 945 public void remove() { 946 throw new UnsupportedOperationException(); 947 } 948 } 949 950 @Override 951 public Collection<DataSource> getDataSources() { 952 return Collections.unmodifiableCollection(dataSources); 953 } 954 955 @Override 956 public synchronized int hashCode() { 957 final int prime = 31; 958 int result = 1; 959 result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode()); 960 result = prime * result + ((privateRoutes == null) ? 0 : privateRoutes.hashCode()); 961 result = prime * result + ((privateTracks == null) ? 0 : privateTracks.hashCode()); 962 result = prime * result + ((privateWaypoints == null) ? 0 : privateWaypoints.hashCode()); 963 return result; 964 } 965 966 @Override 967 public synchronized boolean equals(Object obj) { 968 if (this == obj) 969 return true; 970 if (obj == null) 971 return false; 972 if (getClass() != obj.getClass()) 973 return false; 974 GpxData other = (GpxData) obj; 975 if (dataSources == null) { 976 if (other.dataSources != null) 977 return false; 978 } else if (!dataSources.equals(other.dataSources)) 979 return false; 980 if (privateRoutes == null) { 981 if (other.privateRoutes != null) 982 return false; 983 } else if (!privateRoutes.equals(other.privateRoutes)) 984 return false; 985 if (privateTracks == null) { 986 if (other.privateTracks != null) 987 return false; 988 } else if (!privateTracks.equals(other.privateTracks)) 989 return false; 990 if (privateWaypoints == null) { 991 if (other.privateWaypoints != null) 992 return false; 993 } else if (!privateWaypoints.equals(other.privateWaypoints)) 994 return false; 995 return true; 996 } 997 998 /** 999 * Adds a listener that gets called whenever the data changed. 1000 * @param listener The listener 1001 * @since 12156 1002 */ 1003 public void addChangeListener(GpxDataChangeListener listener) { 1004 listeners.addListener(listener); 1005 } 1006 1007 /** 1008 * Adds a listener that gets called whenever the data changed. It is added with a weak link 1009 * @param listener The listener 1010 */ 1011 public void addWeakChangeListener(GpxDataChangeListener listener) { 1012 listeners.addWeakListener(listener); 1013 } 1014 1015 /** 1016 * Removes a listener that gets called whenever the data changed. 1017 * @param listener The listener 1018 * @since 12156 1019 */ 1020 public void removeChangeListener(GpxDataChangeListener listener) { 1021 listeners.removeListener(listener); 1022 } 1023 1024 private void fireInvalidate() { 1025 if (listeners.hasListeners()) { 1026 GpxDataChangeEvent e = new GpxDataChangeEvent(this); 1027 listeners.fireEvent(l -> l.gpxDataChanged(e)); 1028 } 1029 } 1030 1031 /** 1032 * A listener that listens to GPX data changes. 1033 * @author Michael Zangl 1034 * @since 12156 1035 */ 1036 @FunctionalInterface 1037 public interface GpxDataChangeListener { 1038 /** 1039 * Called when the gpx data changed. 1040 * @param e The event 1041 */ 1042 void gpxDataChanged(GpxDataChangeEvent e); 1043 } 1044 1045 /** 1046 * A data change event in any of the gpx data. 1047 * @author Michael Zangl 1048 * @since 12156 1049 */ 1050 public static class GpxDataChangeEvent { 1051 private final GpxData source; 1052 1053 GpxDataChangeEvent(GpxData source) { 1054 super(); 1055 this.source = source; 1056 } 1057 1058 /** 1059 * Get the data that was changed. 1060 * @return The data. 1061 */ 1062 public GpxData getSource() { 1063 return source; 1064 } 1065 } 1066}