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