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

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

fix #15573 - add synchronization to GpxData

  • Property svn:eol-style set to native
File size: 24.6 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.Collection;
8import java.util.Collections;
9import java.util.Date;
10import java.util.DoubleSummaryStatistics;
11import java.util.HashSet;
12import java.util.Iterator;
13import java.util.Map;
14import java.util.NoSuchElementException;
15import java.util.Set;
16import java.util.stream.Stream;
17
18import org.openstreetmap.josm.Main;
19import org.openstreetmap.josm.data.Bounds;
20import org.openstreetmap.josm.data.Data;
21import org.openstreetmap.josm.data.DataSource;
22import org.openstreetmap.josm.data.coor.EastNorth;
23import org.openstreetmap.josm.data.gpx.GpxTrack.GpxTrackChangeListener;
24import org.openstreetmap.josm.tools.ListenerList;
25import org.openstreetmap.josm.tools.ListeningCollection;
26
27/**
28 * Objects of this class represent a gpx file with tracks, waypoints and routes.
29 * It uses GPX v1.1, see <a href="http://www.topografix.com/GPX/1/1/">the spec</a>
30 * for details.
31 *
32 * @author Raphael Mack &lt;ramack@raphael-mack.de&gt;
33 */
34public class GpxData extends WithAttributes implements Data {
35
36 /**
37 * The disk file this layer is stored in, if it is a local layer. May be <code>null</code>.
38 */
39 public File storageFile;
40 /**
41 * A boolean flag indicating if the data was read from the OSM server.
42 */
43 public boolean fromServer;
44
45 /**
46 * Creator metadata for this file (usually software)
47 */
48 public String creator;
49
50 /**
51 * A list of tracks this file consists of
52 */
53 private final ArrayList<GpxTrack> privateTracks = new ArrayList<>();
54 /**
55 * GXP routes in this file
56 */
57 private final ArrayList<GpxRoute> privateRoutes = new ArrayList<>();
58 /**
59 * Addidionaly waypoints for this file.
60 */
61 private final ArrayList<WayPoint> privateWaypoints = new ArrayList<>();
62 private final GpxTrackChangeListener proxy = e -> fireInvalidate();
63
64 /**
65 * Tracks. Access is discouraged, use {@link #getTracks()} to read.
66 * @see #getTracks()
67 */
68 public final Collection<GpxTrack> tracks = new ListeningCollection<GpxTrack>(privateTracks, this::fireInvalidate) {
69
70 @Override
71 protected void removed(GpxTrack cursor) {
72 cursor.removeListener(proxy);
73 super.removed(cursor);
74 }
75
76 @Override
77 protected void added(GpxTrack cursor) {
78 super.added(cursor);
79 cursor.addListener(proxy);
80 }
81 };
82
83 /**
84 * Routes. Access is discouraged, use {@link #getTracks()} to read.
85 * @see #getRoutes()
86 */
87 public final Collection<GpxRoute> routes = new ListeningCollection<>(privateRoutes, this::fireInvalidate);
88
89 /**
90 * Waypoints. Access is discouraged, use {@link #getTracks()} to read.
91 * @see #getWaypoints()
92 */
93 public final Collection<WayPoint> waypoints = new ListeningCollection<>(privateWaypoints, this::fireInvalidate);
94
95 /**
96 * All data sources (bounds of downloaded bounds) of this GpxData.<br>
97 * Not part of GPX standard but rather a JOSM extension, needed by the fact that
98 * OSM API does not provide {@code <bounds>} element in its GPX reply.
99 * @since 7575
100 */
101 public final Set<DataSource> dataSources = new HashSet<>();
102
103 private final ListenerList<GpxDataChangeListener> listeners = ListenerList.create();
104
105 /**
106 * Merges data from another object.
107 * @param other existing GPX data
108 */
109 public synchronized void mergeFrom(GpxData other) {
110 if (storageFile == null && other.storageFile != null) {
111 storageFile = other.storageFile;
112 }
113 fromServer = fromServer && other.fromServer;
114
115 for (Map.Entry<String, Object> ent : other.attr.entrySet()) {
116 // TODO: Detect conflicts.
117 String k = ent.getKey();
118 if (META_LINKS.equals(k) && attr.containsKey(META_LINKS)) {
119 Collection<GpxLink> my = super.<GpxLink>getCollection(META_LINKS);
120 @SuppressWarnings("unchecked")
121 Collection<GpxLink> their = (Collection<GpxLink>) ent.getValue();
122 my.addAll(their);
123 } else {
124 put(k, ent.getValue());
125 }
126 }
127 other.privateTracks.forEach(this::addTrack);
128 other.privateRoutes.forEach(this::addRoute);
129 other.privateWaypoints.forEach(this::addWaypoint);
130 dataSources.addAll(other.dataSources);
131 fireInvalidate();
132 }
133
134 /**
135 * Get all tracks contained in this data set.
136 * @return The tracks.
137 */
138 public synchronized Collection<GpxTrack> getTracks() {
139 return Collections.unmodifiableCollection(privateTracks);
140 }
141
142 /**
143 * Add a new track
144 * @param track The new track
145 * @since 12156
146 */
147 public synchronized void addTrack(GpxTrack track) {
148 if (privateTracks.stream().anyMatch(t -> t == track)) {
149 throw new IllegalArgumentException(MessageFormat.format("The track was already added to this data: {0}", track));
150 }
151 privateTracks.add(track);
152 track.addListener(proxy);
153 fireInvalidate();
154 }
155
156 /**
157 * Remove a track
158 * @param track The old track
159 * @since 12156
160 */
161 public synchronized void removeTrack(GpxTrack track) {
162 if (!privateTracks.removeIf(t -> t == track)) {
163 throw new IllegalArgumentException(MessageFormat.format("The track was not in this data: {0}", track));
164 }
165 track.removeListener(proxy);
166 fireInvalidate();
167 }
168
169 /**
170 * Gets the list of all routes defined in this data set.
171 * @return The routes
172 * @since 12156
173 */
174 public synchronized Collection<GpxRoute> getRoutes() {
175 return Collections.unmodifiableCollection(privateRoutes);
176 }
177
178 /**
179 * Add a new route
180 * @param route The new route
181 * @since 12156
182 */
183 public synchronized void addRoute(GpxRoute route) {
184 if (privateRoutes.stream().anyMatch(r -> r == route)) {
185 throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", route));
186 }
187 privateRoutes.add(route);
188 fireInvalidate();
189 }
190
191 /**
192 * Remove a route
193 * @param route The old route
194 * @since 12156
195 */
196 public synchronized void removeRoute(GpxRoute route) {
197 if (!privateRoutes.removeIf(r -> r == route)) {
198 throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", route));
199 }
200 fireInvalidate();
201 }
202
203 /**
204 * Gets a list of all way points in this data set.
205 * @return The way points.
206 * @since 12156
207 */
208 public synchronized Collection<WayPoint> getWaypoints() {
209 return Collections.unmodifiableCollection(privateWaypoints);
210 }
211
212 /**
213 * Add a new waypoint
214 * @param waypoint The new waypoint
215 * @since 12156
216 */
217 public synchronized void addWaypoint(WayPoint waypoint) {
218 if (privateWaypoints.stream().anyMatch(w -> w == waypoint)) {
219 throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", waypoint));
220 }
221 privateWaypoints.add(waypoint);
222 fireInvalidate();
223 }
224
225 /**
226 * Remove a waypoint
227 * @param waypoint The old waypoint
228 * @since 12156
229 */
230 public synchronized void removeWaypoint(WayPoint waypoint) {
231 if (!privateWaypoints.removeIf(w -> w == waypoint)) {
232 throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", waypoint));
233 }
234 fireInvalidate();
235 }
236
237 /**
238 * Determines if this GPX data has one or more track points
239 * @return {@code true} if this GPX data has track points, {@code false} otherwise
240 */
241 public synchronized boolean hasTrackPoints() {
242 return getTrackPoints().findAny().isPresent();
243 }
244
245 /**
246 * Gets a stream of all track points in the segments of the tracks of this data.
247 * @return The stream
248 * @see #getTracks()
249 * @see GpxTrack#getSegments()
250 * @see GpxTrackSegment#getWayPoints()
251 * @since 12156
252 */
253 public synchronized Stream<WayPoint> getTrackPoints() {
254 return getTracks().stream().flatMap(trk -> trk.getSegments().stream()).flatMap(trkseg -> trkseg.getWayPoints().stream());
255 }
256
257 /**
258 * Determines if this GPX data has one or more route points
259 * @return {@code true} if this GPX data has route points, {@code false} otherwise
260 */
261 public synchronized boolean hasRoutePoints() {
262 return privateRoutes.stream().anyMatch(rte -> !rte.routePoints.isEmpty());
263 }
264
265 /**
266 * Determines if this GPX data is empty (i.e. does not contain any point)
267 * @return {@code true} if this GPX data is empty, {@code false} otherwise
268 */
269 public synchronized boolean isEmpty() {
270 return !hasRoutePoints() && !hasTrackPoints() && waypoints.isEmpty();
271 }
272
273 /**
274 * Returns the bounds defining the extend of this data, as read in metadata, if any.
275 * If no bounds is defined in metadata, {@code null} is returned. There is no guarantee
276 * that data entirely fit in this bounds, as it is not recalculated. To get recalculated bounds,
277 * see {@link #recalculateBounds()}. To get downloaded areas, see {@link #dataSources}.
278 * @return the bounds defining the extend of this data, or {@code null}.
279 * @see #recalculateBounds()
280 * @see #dataSources
281 * @since 7575
282 */
283 public Bounds getMetaBounds() {
284 Object value = get(META_BOUNDS);
285 if (value instanceof Bounds) {
286 return (Bounds) value;
287 }
288 return null;
289 }
290
291 /**
292 * Calculates the bounding box of available data and returns it.
293 * The bounds are not stored internally, but recalculated every time
294 * this function is called.<br>
295 * To get bounds as read from metadata, see {@link #getMetaBounds()}.<br>
296 * To get downloaded areas, see {@link #dataSources}.<br>
297 *
298 * FIXME might perhaps use visitor pattern?
299 * @return the bounds
300 * @see #getMetaBounds()
301 * @see #dataSources
302 */
303 public synchronized Bounds recalculateBounds() {
304 Bounds bounds = null;
305 for (WayPoint wpt : privateWaypoints) {
306 if (bounds == null) {
307 bounds = new Bounds(wpt.getCoor());
308 } else {
309 bounds.extend(wpt.getCoor());
310 }
311 }
312 for (GpxRoute rte : privateRoutes) {
313 for (WayPoint wpt : rte.routePoints) {
314 if (bounds == null) {
315 bounds = new Bounds(wpt.getCoor());
316 } else {
317 bounds.extend(wpt.getCoor());
318 }
319 }
320 }
321 for (GpxTrack trk : privateTracks) {
322 Bounds trkBounds = trk.getBounds();
323 if (trkBounds != null) {
324 if (bounds == null) {
325 bounds = new Bounds(trkBounds);
326 } else {
327 bounds.extend(trkBounds);
328 }
329 }
330 }
331 return bounds;
332 }
333
334 /**
335 * calculates the sum of the lengths of all track segments
336 * @return the length in meters
337 */
338 public synchronized double length() {
339 return privateTracks.stream().mapToDouble(GpxTrack::length).sum();
340 }
341
342 /**
343 * returns minimum and maximum timestamps in the track
344 * @param trk track to analyze
345 * @return minimum and maximum dates in array of 2 elements
346 */
347 public static Date[] getMinMaxTimeForTrack(GpxTrack trk) {
348 final DoubleSummaryStatistics statistics = trk.getSegments().stream()
349 .flatMap(seg -> seg.getWayPoints().stream())
350 .mapToDouble(pnt -> pnt.time)
351 .summaryStatistics();
352 return statistics.getCount() == 0
353 ? null
354 : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 1000))};
355 }
356
357 /**
358 * Returns minimum and maximum timestamps for all tracks
359 * Warning: there are lot of track with broken timestamps,
360 * so we just ingore points from future and from year before 1970 in this method
361 * works correctly @since 5815
362 * @return minimum and maximum dates in array of 2 elements
363 */
364 public synchronized Date[] getMinMaxTimeForAllTracks() {
365 double now = System.currentTimeMillis() / 1000.0;
366 final DoubleSummaryStatistics statistics = tracks.stream()
367 .flatMap(trk -> trk.getSegments().stream())
368 .flatMap(seg -> seg.getWayPoints().stream())
369 .mapToDouble(pnt -> pnt.time)
370 .filter(t -> t > 0 && t <= now)
371 .summaryStatistics();
372 return statistics.getCount() == 0
373 ? new Date[0]
374 : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 1000))};
375 }
376
377 /**
378 * Makes a WayPoint at the projection of point p onto the track providing p is less than
379 * tolerance away from the track
380 *
381 * @param p : the point to determine the projection for
382 * @param tolerance : must be no further than this from the track
383 * @return the closest point on the track to p, which may be the first or last point if off the
384 * end of a segment, or may be null if nothing close enough
385 */
386 public synchronized WayPoint nearestPointOnTrack(EastNorth p, double tolerance) {
387 /*
388 * assume the coordinates of P are xp,yp, and those of a section of track between two
389 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
390 *
391 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
392 *
393 * Also, note that the distance RS^2 is A^2 + B^2
394 *
395 * If RS^2 == 0.0 ignore the degenerate section of track
396 *
397 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
398 *
399 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line
400 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
401 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
402 *
403 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
404 *
405 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
406 *
407 * where RN = sqrt(PR^2 - PN^2)
408 */
409
410 double pnminsq = tolerance * tolerance;
411 EastNorth bestEN = null;
412 double bestTime = 0.0;
413 double px = p.east();
414 double py = p.north();
415 double rx = 0.0, ry = 0.0, sx, sy, x, y;
416 for (GpxTrack track : privateTracks) {
417 for (GpxTrackSegment seg : track.getSegments()) {
418 WayPoint r = null;
419 for (WayPoint wpSeg : seg.getWayPoints()) {
420 EastNorth en = wpSeg.getEastNorth(Main.getProjection());
421 if (r == null) {
422 r = wpSeg;
423 rx = en.east();
424 ry = en.north();
425 x = px - rx;
426 y = py - ry;
427 double pRsq = x * x + y * y;
428 if (pRsq < pnminsq) {
429 pnminsq = pRsq;
430 bestEN = en;
431 bestTime = r.time;
432 }
433 } else {
434 sx = en.east();
435 sy = en.north();
436 double a = sy - ry;
437 double b = rx - sx;
438 double c = -a * rx - b * ry;
439 double rssq = a * a + b * b;
440 if (rssq == 0) {
441 continue;
442 }
443 double pnsq = a * px + b * py + c;
444 pnsq = pnsq * pnsq / rssq;
445 if (pnsq < pnminsq) {
446 x = px - rx;
447 y = py - ry;
448 double prsq = x * x + y * y;
449 x = px - sx;
450 y = py - sy;
451 double pssq = x * x + y * y;
452 if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) {
453 double rnoverRS = Math.sqrt((prsq - pnsq) / rssq);
454 double nx = rx - rnoverRS * b;
455 double ny = ry + rnoverRS * a;
456 bestEN = new EastNorth(nx, ny);
457 bestTime = r.time + rnoverRS * (wpSeg.time - r.time);
458 pnminsq = pnsq;
459 }
460 }
461 r = wpSeg;
462 rx = sx;
463 ry = sy;
464 }
465 }
466 if (r != null) {
467 EastNorth c = r.getEastNorth(Main.getProjection());
468 /* if there is only one point in the seg, it will do this twice, but no matter */
469 rx = c.east();
470 ry = c.north();
471 x = px - rx;
472 y = py - ry;
473 double prsq = x * x + y * y;
474 if (prsq < pnminsq) {
475 pnminsq = prsq;
476 bestEN = c;
477 bestTime = r.time;
478 }
479 }
480 }
481 }
482 if (bestEN == null)
483 return null;
484 WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN));
485 best.time = bestTime;
486 return best;
487 }
488
489 /**
490 * Iterate over all track segments and over all routes.
491 *
492 * @param trackVisibility An array indicating which tracks should be
493 * included in the iteration. Can be null, then all tracks are included.
494 * @return an Iterable object, which iterates over all track segments and
495 * over all routes
496 */
497 public Iterable<Collection<WayPoint>> getLinesIterable(final boolean... trackVisibility) {
498 return () -> new LinesIterator(this, trackVisibility);
499 }
500
501 /**
502 * Resets the internal caches of east/north coordinates.
503 */
504 public synchronized void resetEastNorthCache() {
505 privateWaypoints.forEach(WayPoint::invalidateEastNorthCache);
506 getTrackPoints().forEach(WayPoint::invalidateEastNorthCache);
507 for (GpxRoute route: getRoutes()) {
508 if (route.routePoints == null) {
509 continue;
510 }
511 for (WayPoint wp: route.routePoints) {
512 wp.invalidateEastNorthCache();
513 }
514 }
515 }
516
517 /**
518 * Iterates over all track segments and then over all routes.
519 */
520 public static class LinesIterator implements Iterator<Collection<WayPoint>> {
521
522 private Iterator<GpxTrack> itTracks;
523 private int idxTracks;
524 private Iterator<GpxTrackSegment> itTrackSegments;
525 private final Iterator<GpxRoute> itRoutes;
526
527 private Collection<WayPoint> next;
528 private final boolean[] trackVisibility;
529
530 /**
531 * Constructs a new {@code LinesIterator}.
532 * @param data GPX data
533 * @param trackVisibility An array indicating which tracks should be
534 * included in the iteration. Can be null, then all tracks are included.
535 */
536 public LinesIterator(GpxData data, boolean... trackVisibility) {
537 itTracks = data.tracks.iterator();
538 idxTracks = -1;
539 itRoutes = data.routes.iterator();
540 this.trackVisibility = trackVisibility;
541 next = getNext();
542 }
543
544 @Override
545 public boolean hasNext() {
546 return next != null;
547 }
548
549 @Override
550 public Collection<WayPoint> next() {
551 if (!hasNext()) {
552 throw new NoSuchElementException();
553 }
554 Collection<WayPoint> current = next;
555 next = getNext();
556 return current;
557 }
558
559 private Collection<WayPoint> getNext() {
560 if (itTracks != null) {
561 if (itTrackSegments != null && itTrackSegments.hasNext()) {
562 return itTrackSegments.next().getWayPoints();
563 } else {
564 while (itTracks.hasNext()) {
565 GpxTrack nxtTrack = itTracks.next();
566 idxTracks++;
567 if (trackVisibility != null && !trackVisibility[idxTracks])
568 continue;
569 itTrackSegments = nxtTrack.getSegments().iterator();
570 if (itTrackSegments.hasNext()) {
571 return itTrackSegments.next().getWayPoints();
572 }
573 }
574 // if we get here, all the Tracks are finished; Continue with Routes
575 itTracks = null;
576 }
577 }
578 if (itRoutes.hasNext()) {
579 return itRoutes.next().routePoints;
580 }
581 return null;
582 }
583
584 @Override
585 public void remove() {
586 throw new UnsupportedOperationException();
587 }
588 }
589
590 @Override
591 public Collection<DataSource> getDataSources() {
592 return Collections.unmodifiableCollection(dataSources);
593 }
594
595 @Override
596 public synchronized int hashCode() {
597 final int prime = 31;
598 int result = 1;
599 result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode());
600 result = prime * result + ((privateRoutes == null) ? 0 : privateRoutes.hashCode());
601 result = prime * result + ((privateTracks == null) ? 0 : privateTracks.hashCode());
602 result = prime * result + ((privateWaypoints == null) ? 0 : privateWaypoints.hashCode());
603 return result;
604 }
605
606 @Override
607 public synchronized boolean equals(Object obj) {
608 if (this == obj)
609 return true;
610 if (obj == null)
611 return false;
612 if (getClass() != obj.getClass())
613 return false;
614 GpxData other = (GpxData) obj;
615 if (dataSources == null) {
616 if (other.dataSources != null)
617 return false;
618 } else if (!dataSources.equals(other.dataSources))
619 return false;
620 if (privateRoutes == null) {
621 if (other.privateRoutes != null)
622 return false;
623 } else if (!privateRoutes.equals(other.privateRoutes))
624 return false;
625 if (privateTracks == null) {
626 if (other.privateTracks != null)
627 return false;
628 } else if (!privateTracks.equals(other.privateTracks))
629 return false;
630 if (privateWaypoints == null) {
631 if (other.privateWaypoints != null)
632 return false;
633 } else if (!privateWaypoints.equals(other.privateWaypoints))
634 return false;
635 return true;
636 }
637
638 /**
639 * Adds a listener that gets called whenever the data changed.
640 * @param listener The listener
641 * @since 12156
642 */
643 public void addChangeListener(GpxDataChangeListener listener) {
644 listeners.addListener(listener);
645 }
646
647 /**
648 * Adds a listener that gets called whenever the data changed. It is added with a weak link
649 * @param listener The listener
650 */
651 public void addWeakChangeListener(GpxDataChangeListener listener) {
652 listeners.addWeakListener(listener);
653 }
654
655 /**
656 * Removes a listener that gets called whenever the data changed.
657 * @param listener The listener
658 * @since 12156
659 */
660 public void removeChangeListener(GpxDataChangeListener listener) {
661 listeners.removeListener(listener);
662 }
663
664 private void fireInvalidate() {
665 if (listeners.hasListeners()) {
666 GpxDataChangeEvent e = new GpxDataChangeEvent(this);
667 listeners.fireEvent(l -> l.gpxDataChanged(e));
668 }
669 }
670
671 /**
672 * A listener that listens to GPX data changes.
673 * @author Michael Zangl
674 * @since 12156
675 */
676 @FunctionalInterface
677 public interface GpxDataChangeListener {
678 /**
679 * Called when the gpx data changed.
680 * @param e The event
681 */
682 void gpxDataChanged(GpxDataChangeEvent e);
683 }
684
685 /**
686 * A data change event in any of the gpx data.
687 * @author Michael Zangl
688 * @since 12156
689 */
690 public static class GpxDataChangeEvent {
691 private final GpxData source;
692
693 GpxDataChangeEvent(GpxData source) {
694 super();
695 this.source = source;
696 }
697
698 /**
699 * Get the data that was changed.
700 * @return The data.
701 */
702 public GpxData getSource() {
703 return source;
704 }
705 }
706}
Note: See TracBrowser for help on using the repository browser.