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

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

fix #16796 - Rework of GPX track colors / layer preferences (patch by Bjoeni)

  • Property svn:eol-style set to native
File size: 44.7 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.util.ArrayList;
7import java.util.Arrays;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.Date;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.Iterator;
14import java.util.List;
15import java.util.LongSummaryStatistics;
16import java.util.Map;
17import java.util.NoSuchElementException;
18import java.util.Objects;
19import java.util.Optional;
20import java.util.Set;
21import java.util.stream.Collectors;
22import java.util.stream.Stream;
23
24import org.openstreetmap.josm.data.Bounds;
25import org.openstreetmap.josm.data.Data;
26import org.openstreetmap.josm.data.DataSource;
27import org.openstreetmap.josm.data.coor.EastNorth;
28import org.openstreetmap.josm.data.gpx.IGpxTrack.GpxTrackChangeListener;
29import org.openstreetmap.josm.data.projection.ProjectionRegistry;
30import org.openstreetmap.josm.gui.MainApplication;
31import org.openstreetmap.josm.gui.layer.GpxLayer;
32import org.openstreetmap.josm.tools.ListenerList;
33import org.openstreetmap.josm.tools.ListeningCollection;
34
35/**
36 * Objects of this class represent a gpx file with tracks, waypoints and routes.
37 * It uses GPX v1.1, see <a href="http://www.topografix.com/GPX/1/1/">the spec</a>
38 * for details.
39 *
40 * @author Raphael Mack &lt;ramack@raphael-mack.de&gt;
41 */
42public class GpxData extends WithAttributes implements Data {
43
44 /**
45 * Constructs a new GpxData.
46 */
47 public GpxData() {}
48
49 /**
50 * Constructs a new GpxData that is currently being initialized, so no listeners will be fired until {@link #endUpdate()} is called.
51 * @param initializing true
52 * @since 15496
53 */
54 public GpxData(boolean initializing) {
55 this.initializing = initializing;
56 }
57
58 /**
59 * The disk file this layer is stored in, if it is a local layer. May be <code>null</code>.
60 */
61 public File storageFile;
62 /**
63 * A boolean flag indicating if the data was read from the OSM server.
64 */
65 public boolean fromServer;
66
67 /**
68 * Creator metadata for this file (usually software)
69 */
70 public String creator;
71
72 /**
73 * A list of tracks this file consists of
74 */
75 private final ArrayList<GpxTrack> privateTracks = new ArrayList<>();
76 /**
77 * GPX routes in this file
78 */
79 private final ArrayList<GpxRoute> privateRoutes = new ArrayList<>();
80 /**
81 * Addidionaly waypoints for this file.
82 */
83 private final ArrayList<WayPoint> privateWaypoints = new ArrayList<>();
84 /**
85 * All namespaces read from the original file
86 */
87 private final List<XMLNamespace> namespaces = new ArrayList<>();
88 /**
89 * The layer specific prefs formerly saved in the preferences, e.g. drawing options.
90 * NOT the track specific settings (e.g. color, width)
91 */
92 private final Map<String, String> layerPrefs = new HashMap<>();
93
94 private final GpxTrackChangeListener proxy = e -> invalidate();
95 private boolean modified, updating, initializing;
96 private boolean suppressedInvalidate;
97
98 /**
99 * Tracks. Access is discouraged, use {@link #getTracks()} to read.
100 * @see #getTracks()
101 */
102 public final Collection<GpxTrack> tracks = new ListeningCollection<GpxTrack>(privateTracks, this::invalidate) {
103
104 @Override
105 protected void removed(GpxTrack cursor) {
106 cursor.removeListener(proxy);
107 super.removed(cursor);
108 }
109
110 @Override
111 protected void added(GpxTrack 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 (GpxTrack 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(GpxTrack 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 (GpxTrack 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<GpxTrack> 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(GpxTrack 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(GpxTrack 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 GpxTrack#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 (GpxTrack 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(GpxTrack::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(GpxTrack 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 (GpxTrack 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<GpxTrack> 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 GpxTrack 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
1211 * @param uri
1212 * @param 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}
Note: See TracBrowser for help on using the repository browser.