001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.gpx;
003
004import java.io.File;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.Date;
008import java.util.DoubleSummaryStatistics;
009import java.util.HashSet;
010import java.util.Iterator;
011import java.util.LinkedList;
012import java.util.Map;
013import java.util.NoSuchElementException;
014import java.util.Set;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.data.Bounds;
018import org.openstreetmap.josm.data.Data;
019import org.openstreetmap.josm.data.DataSource;
020import org.openstreetmap.josm.data.coor.EastNorth;
021
022/**
023 * Objects of this class represent a gpx file with tracks, waypoints and routes.
024 * It uses GPX v1.1, see <a href="http://www.topografix.com/GPX/1/1/">the spec</a>
025 * for details.
026 *
027 * @author Raphael Mack &lt;ramack@raphael-mack.de&gt;
028 */
029public class GpxData extends WithAttributes implements Data {
030
031    public File storageFile;
032    public boolean fromServer;
033
034    /** Creator (usually software) */
035    public String creator;
036
037    /** Tracks */
038    public final Collection<GpxTrack> tracks = new LinkedList<>();
039    /** Routes */
040    public final Collection<GpxRoute> routes = new LinkedList<>();
041    /** Waypoints */
042    public final Collection<WayPoint> waypoints = new LinkedList<>();
043
044    /**
045     * All data sources (bounds of downloaded bounds) of this GpxData.<br>
046     * Not part of GPX standard but rather a JOSM extension, needed by the fact that
047     * OSM API does not provide {@code <bounds>} element in its GPX reply.
048     * @since 7575
049     */
050    public final Set<DataSource> dataSources = new HashSet<>();
051
052    /**
053     * Merges data from another object.
054     * @param other existing GPX data
055     */
056    public void mergeFrom(GpxData other) {
057        if (storageFile == null && other.storageFile != null) {
058            storageFile = other.storageFile;
059        }
060        fromServer = fromServer && other.fromServer;
061
062        for (Map.Entry<String, Object> ent : other.attr.entrySet()) {
063            // TODO: Detect conflicts.
064            String k = ent.getKey();
065            if (META_LINKS.equals(k) && attr.containsKey(META_LINKS)) {
066                Collection<GpxLink> my = super.<GpxLink>getCollection(META_LINKS);
067                @SuppressWarnings("unchecked")
068                Collection<GpxLink> their = (Collection<GpxLink>) ent.getValue();
069                my.addAll(their);
070            } else {
071                put(k, ent.getValue());
072            }
073        }
074        tracks.addAll(other.tracks);
075        routes.addAll(other.routes);
076        waypoints.addAll(other.waypoints);
077        dataSources.addAll(other.dataSources);
078    }
079
080    /**
081     * Determines if this GPX data has one or more track points
082     * @return {@code true} if this GPX data has track points, {@code false} otherwise
083     */
084    public boolean hasTrackPoints() {
085        for (GpxTrack trk : tracks) {
086            for (GpxTrackSegment trkseg : trk.getSegments()) {
087                if (!trkseg.getWayPoints().isEmpty())
088                    return true;
089            }
090        }
091        return false;
092    }
093
094    /**
095     * Determines if this GPX data has one or more route points
096     * @return {@code true} if this GPX data has route points, {@code false} otherwise
097     */
098    public boolean hasRoutePoints() {
099        for (GpxRoute rte : routes) {
100            if (!rte.routePoints.isEmpty())
101                return true;
102        }
103        return false;
104    }
105
106    /**
107     * Determines if this GPX data is empty (i.e. does not contain any point)
108     * @return {@code true} if this GPX data is empty, {@code false} otherwise
109     */
110    public boolean isEmpty() {
111        return !hasRoutePoints() && !hasTrackPoints() && waypoints.isEmpty();
112    }
113
114    /**
115     * Returns the bounds defining the extend of this data, as read in metadata, if any.
116     * If no bounds is defined in metadata, {@code null} is returned. There is no guarantee
117     * that data entirely fit in this bounds, as it is not recalculated. To get recalculated bounds,
118     * see {@link #recalculateBounds()}. To get downloaded areas, see {@link #dataSources}.
119     * @return the bounds defining the extend of this data, or {@code null}.
120     * @see #recalculateBounds()
121     * @see #dataSources
122     * @since 7575
123     */
124    public Bounds getMetaBounds() {
125        Object value = get(META_BOUNDS);
126        if (value instanceof Bounds) {
127            return (Bounds) value;
128        }
129        return null;
130    }
131
132    /**
133     * Calculates the bounding box of available data and returns it.
134     * The bounds are not stored internally, but recalculated every time
135     * this function is called.<br>
136     * To get bounds as read from metadata, see {@link #getMetaBounds()}.<br>
137     * To get downloaded areas, see {@link #dataSources}.<br>
138     *
139     * FIXME might perhaps use visitor pattern?
140     * @return the bounds
141     * @see #getMetaBounds()
142     * @see #dataSources
143     */
144    public Bounds recalculateBounds() {
145        Bounds bounds = null;
146        for (WayPoint wpt : waypoints) {
147            if (bounds == null) {
148                bounds = new Bounds(wpt.getCoor());
149            } else {
150                bounds.extend(wpt.getCoor());
151            }
152        }
153        for (GpxRoute rte : routes) {
154            for (WayPoint wpt : rte.routePoints) {
155                if (bounds == null) {
156                    bounds = new Bounds(wpt.getCoor());
157                } else {
158                    bounds.extend(wpt.getCoor());
159                }
160            }
161        }
162        for (GpxTrack trk : tracks) {
163            Bounds trkBounds = trk.getBounds();
164            if (trkBounds != null) {
165                if (bounds == null) {
166                    bounds = new Bounds(trkBounds);
167                } else {
168                    bounds.extend(trkBounds);
169                }
170            }
171        }
172        return bounds;
173    }
174
175    /**
176     * calculates the sum of the lengths of all track segments
177     * @return the length in meters
178     */
179    public double length() {
180        double result = 0.0; // in meters
181
182        for (GpxTrack trk : tracks) {
183            result += trk.length();
184        }
185
186        return result;
187    }
188
189    /**
190     * returns minimum and maximum timestamps in the track
191     * @param trk track to analyze
192     * @return  minimum and maximum dates in array of 2 elements
193     */
194    public static Date[] getMinMaxTimeForTrack(GpxTrack trk) {
195        final DoubleSummaryStatistics statistics = trk.getSegments().stream()
196                .flatMap(seg -> seg.getWayPoints().stream())
197                .mapToDouble(pnt -> pnt.time)
198                .summaryStatistics();
199        return statistics.getCount() == 0
200                ? null
201                : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 1000))};
202    }
203
204    /**
205    * Returns minimum and maximum timestamps for all tracks
206    * Warning: there are lot of track with broken timestamps,
207    * so we just ingore points from future and from year before 1970 in this method
208    * works correctly @since 5815
209     * @return minimum and maximum dates in array of 2 elements
210    */
211    public Date[] getMinMaxTimeForAllTracks() {
212        double now = System.currentTimeMillis() / 1000.0;
213        final DoubleSummaryStatistics statistics = tracks.stream()
214                .flatMap(trk -> trk.getSegments().stream())
215                .flatMap(seg -> seg.getWayPoints().stream())
216                .mapToDouble(pnt -> pnt.time)
217                .filter(t -> t > 0 && t <= now)
218                .summaryStatistics();
219        return statistics.getCount() == 0
220                ? new Date[0]
221                : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 1000))};
222    }
223
224    /**
225     * Makes a WayPoint at the projection of point p onto the track providing p is less than
226     * tolerance away from the track
227     *
228     * @param p : the point to determine the projection for
229     * @param tolerance : must be no further than this from the track
230     * @return the closest point on the track to p, which may be the first or last point if off the
231     * end of a segment, or may be null if nothing close enough
232     */
233    public WayPoint nearestPointOnTrack(EastNorth p, double tolerance) {
234        /*
235         * assume the coordinates of P are xp,yp, and those of a section of track between two
236         * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
237         *
238         * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
239         *
240         * Also, note that the distance RS^2 is A^2 + B^2
241         *
242         * If RS^2 == 0.0 ignore the degenerate section of track
243         *
244         * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
245         *
246         * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line
247         * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
248         * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
249         *
250         * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
251         *
252         * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
253         *
254         * where RN = sqrt(PR^2 - PN^2)
255         */
256
257        double pnminsq = tolerance * tolerance;
258        EastNorth bestEN = null;
259        double bestTime = 0.0;
260        double px = p.east();
261        double py = p.north();
262        double rx = 0.0, ry = 0.0, sx, sy, x, y;
263        if (tracks == null)
264            return null;
265        for (GpxTrack track : tracks) {
266            for (GpxTrackSegment seg : track.getSegments()) {
267                WayPoint r = null;
268                for (WayPoint S : seg.getWayPoints()) {
269                    EastNorth en = S.getEastNorth();
270                    if (r == null) {
271                        r = S;
272                        rx = en.east();
273                        ry = en.north();
274                        x = px - rx;
275                        y = py - ry;
276                        double pRsq = x * x + y * y;
277                        if (pRsq < pnminsq) {
278                            pnminsq = pRsq;
279                            bestEN = en;
280                            bestTime = r.time;
281                        }
282                    } else {
283                        sx = en.east();
284                        sy = en.north();
285                        double a = sy - ry;
286                        double b = rx - sx;
287                        double c = -a * rx - b * ry;
288                        double rssq = a * a + b * b;
289                        if (rssq == 0) {
290                            continue;
291                        }
292                        double pnsq = a * px + b * py + c;
293                        pnsq = pnsq * pnsq / rssq;
294                        if (pnsq < pnminsq) {
295                            x = px - rx;
296                            y = py - ry;
297                            double prsq = x * x + y * y;
298                            x = px - sx;
299                            y = py - sy;
300                            double pssq = x * x + y * y;
301                            if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) {
302                                double rnoverRS = Math.sqrt((prsq - pnsq) / rssq);
303                                double nx = rx - rnoverRS * b;
304                                double ny = ry + rnoverRS * a;
305                                bestEN = new EastNorth(nx, ny);
306                                bestTime = r.time + rnoverRS * (S.time - r.time);
307                                pnminsq = pnsq;
308                            }
309                        }
310                        r = S;
311                        rx = sx;
312                        ry = sy;
313                    }
314                }
315                if (r != null) {
316                    EastNorth c = r.getEastNorth();
317                    /* if there is only one point in the seg, it will do this twice, but no matter */
318                    rx = c.east();
319                    ry = c.north();
320                    x = px - rx;
321                    y = py - ry;
322                    double prsq = x * x + y * y;
323                    if (prsq < pnminsq) {
324                        pnminsq = prsq;
325                        bestEN = c;
326                        bestTime = r.time;
327                    }
328                }
329            }
330        }
331        if (bestEN == null)
332            return null;
333        WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN));
334        best.time = bestTime;
335        return best;
336    }
337
338    /**
339     * Iterate over all track segments and over all routes.
340     *
341     * @param trackVisibility An array indicating which tracks should be
342     * included in the iteration. Can be null, then all tracks are included.
343     * @return an Iterable object, which iterates over all track segments and
344     * over all routes
345     */
346    public Iterable<Collection<WayPoint>> getLinesIterable(final boolean ... trackVisibility) {
347        return () -> new LinesIterator(this, trackVisibility);
348    }
349
350    /**
351     * Resets the internal caches of east/north coordinates.
352     */
353    public void resetEastNorthCache() {
354        if (waypoints != null) {
355            for (WayPoint wp : waypoints) {
356                wp.invalidateEastNorthCache();
357            }
358        }
359        if (tracks != null) {
360            for (GpxTrack track: tracks) {
361                for (GpxTrackSegment segment: track.getSegments()) {
362                    for (WayPoint wp: segment.getWayPoints()) {
363                        wp.invalidateEastNorthCache();
364                    }
365                }
366            }
367        }
368        if (routes != null) {
369            for (GpxRoute route: routes) {
370                if (route.routePoints == null) {
371                    continue;
372                }
373                for (WayPoint wp: route.routePoints) {
374                    wp.invalidateEastNorthCache();
375                }
376            }
377        }
378    }
379
380    /**
381     * Iterates over all track segments and then over all routes.
382     */
383    public static class LinesIterator implements Iterator<Collection<WayPoint>> {
384
385        private Iterator<GpxTrack> itTracks;
386        private int idxTracks;
387        private Iterator<GpxTrackSegment> itTrackSegments;
388        private final Iterator<GpxRoute> itRoutes;
389
390        private Collection<WayPoint> next;
391        private final boolean[] trackVisibility;
392
393        /**
394         * Constructs a new {@code LinesIterator}.
395         * @param data GPX data
396         * @param trackVisibility An array indicating which tracks should be
397         * included in the iteration. Can be null, then all tracks are included.
398         */
399        public LinesIterator(GpxData data, boolean ... trackVisibility) {
400            itTracks = data.tracks.iterator();
401            idxTracks = -1;
402            itRoutes = data.routes.iterator();
403            this.trackVisibility = trackVisibility;
404            next = getNext();
405        }
406
407        @Override
408        public boolean hasNext() {
409            return next != null;
410        }
411
412        @Override
413        public Collection<WayPoint> next() {
414            if (!hasNext()) {
415                throw new NoSuchElementException();
416            }
417            Collection<WayPoint> current = next;
418            next = getNext();
419            return current;
420        }
421
422        private Collection<WayPoint> getNext() {
423            if (itTracks != null) {
424                if (itTrackSegments != null && itTrackSegments.hasNext()) {
425                    return itTrackSegments.next().getWayPoints();
426                } else {
427                    while (itTracks.hasNext()) {
428                        GpxTrack nxtTrack = itTracks.next();
429                        idxTracks++;
430                        if (trackVisibility != null && !trackVisibility[idxTracks])
431                            continue;
432                        itTrackSegments = nxtTrack.getSegments().iterator();
433                        if (itTrackSegments.hasNext()) {
434                            return itTrackSegments.next().getWayPoints();
435                        }
436                    }
437                    // if we get here, all the Tracks are finished; Continue with Routes
438                    itTracks = null;
439                }
440            }
441            if (itRoutes.hasNext()) {
442                return itRoutes.next().routePoints;
443            }
444            return null;
445        }
446
447        @Override
448        public void remove() {
449            throw new UnsupportedOperationException();
450        }
451    }
452
453    @Override
454    public Collection<DataSource> getDataSources() {
455        return Collections.unmodifiableCollection(dataSources);
456    }
457
458    @Override
459    public int hashCode() {
460        final int prime = 31;
461        int result = 1;
462        result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode());
463        result = prime * result + ((routes == null) ? 0 : routes.hashCode());
464        result = prime * result + ((tracks == null) ? 0 : tracks.hashCode());
465        result = prime * result + ((waypoints == null) ? 0 : waypoints.hashCode());
466        return result;
467    }
468
469    @Override
470    public boolean equals(Object obj) {
471        if (this == obj)
472            return true;
473        if (obj == null)
474            return false;
475        if (getClass() != obj.getClass())
476            return false;
477        GpxData other = (GpxData) obj;
478        if (dataSources == null) {
479            if (other.dataSources != null)
480                return false;
481        } else if (!dataSources.equals(other.dataSources))
482            return false;
483        if (routes == null) {
484            if (other.routes != null)
485                return false;
486        } else if (!routes.equals(other.routes))
487            return false;
488        if (tracks == null) {
489            if (other.tracks != null)
490                return false;
491        } else if (!tracks.equals(other.tracks))
492            return false;
493        if (waypoints == null) {
494            if (other.waypoints != null)
495                return false;
496        } else if (!waypoints.equals(other.waypoints))
497            return false;
498        return true;
499    }
500}