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

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

fix #18012 - create track segment names from gpx layer name if there is no 'name' attribute present in the actual gpx (patch by cmuelle8)

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