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