source: josm/trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java@ 18207

Last change on this file since 18207 was 18207, checked in by Don-vip, 3 years ago

fix #21257 - sort tracks chronologically when writing GPX file

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