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