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.HashSet;
009import java.util.Iterator;
010import java.util.LinkedList;
011import java.util.Map;
012import java.util.NoSuchElementException;
013import java.util.Set;
014
015import org.openstreetmap.josm.Main;
016import org.openstreetmap.josm.data.Bounds;
017import org.openstreetmap.josm.data.Data;
018import org.openstreetmap.josm.data.DataSource;
019import org.openstreetmap.josm.data.coor.EastNorth;
020import org.openstreetmap.josm.tools.Utils;
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        WayPoint earliest = null, latest = null;
196
197        for (GpxTrackSegment seg : trk.getSegments()) {
198            for (WayPoint pnt : seg.getWayPoints()) {
199                if (latest == null) {
200                    latest = earliest = pnt;
201                } else {
202                    if (pnt.compareTo(earliest) < 0) {
203                        earliest = pnt;
204                    } else if (pnt.compareTo(latest) > 0) {
205                        latest = pnt;
206                    }
207                }
208            }
209        }
210        if (earliest == null || latest == null) return null;
211        return new Date[]{earliest.getTime(), latest.getTime()};
212    }
213
214    /**
215    * Returns minimum and maximum timestamps for all tracks
216    * Warning: there are lot of track with broken timestamps,
217    * so we just ingore points from future and from year before 1970 in this method
218    * works correctly @since 5815
219     * @return minimum and maximum dates in array of 2 elements
220    */
221    public Date[] getMinMaxTimeForAllTracks() {
222        double min = 1e100;
223        double max = -1e100;
224        double now = System.currentTimeMillis()/1000.0;
225        for (GpxTrack trk: tracks) {
226            for (GpxTrackSegment seg : trk.getSegments()) {
227                for (WayPoint pnt : seg.getWayPoints()) {
228                    double t = pnt.time;
229                    if (t > 0 && t <= now) {
230                        if (t > max) max = t;
231                        if (t < min) min = t;
232                    }
233                }
234            }
235        }
236        if (Utils.equalsEpsilon(min, 1e100) || Utils.equalsEpsilon(max, -1e100)) return new Date[0];
237        return new Date[]{new Date((long) (min * 1000)), new Date((long) (max * 1000))};
238    }
239
240    /**
241     * Makes a WayPoint at the projection of point p onto the track providing p is less than
242     * tolerance away from the track
243     *
244     * @param p : the point to determine the projection for
245     * @param tolerance : must be no further than this from the track
246     * @return the closest point on the track to p, which may be the first or last point if off the
247     * end of a segment, or may be null if nothing close enough
248     */
249    public WayPoint nearestPointOnTrack(EastNorth p, double tolerance) {
250        /*
251         * assume the coordinates of P are xp,yp, and those of a section of track between two
252         * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
253         *
254         * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
255         *
256         * Also, note that the distance RS^2 is A^2 + B^2
257         *
258         * If RS^2 == 0.0 ignore the degenerate section of track
259         *
260         * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
261         *
262         * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line
263         * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
264         * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
265         *
266         * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
267         *
268         * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
269         *
270         * where RN = sqrt(PR^2 - PN^2)
271         */
272
273        double pnminsq = tolerance * tolerance;
274        EastNorth bestEN = null;
275        double bestTime = 0.0;
276        double px = p.east();
277        double py = p.north();
278        double rx = 0.0, ry = 0.0, sx, sy, x, y;
279        if (tracks == null)
280            return null;
281        for (GpxTrack track : tracks) {
282            for (GpxTrackSegment seg : track.getSegments()) {
283                WayPoint r = null;
284                for (WayPoint S : seg.getWayPoints()) {
285                    EastNorth en = S.getEastNorth();
286                    if (r == null) {
287                        r = S;
288                        rx = en.east();
289                        ry = en.north();
290                        x = px - rx;
291                        y = py - ry;
292                        double pRsq = x * x + y * y;
293                        if (pRsq < pnminsq) {
294                            pnminsq = pRsq;
295                            bestEN = en;
296                            bestTime = r.time;
297                        }
298                    } else {
299                        sx = en.east();
300                        sy = en.north();
301                        double a = sy - ry;
302                        double b = rx - sx;
303                        double c = -a * rx - b * ry;
304                        double rssq = a * a + b * b;
305                        if (rssq == 0) {
306                            continue;
307                        }
308                        double pnsq = a * px + b * py + c;
309                        pnsq = pnsq * pnsq / rssq;
310                        if (pnsq < pnminsq) {
311                            x = px - rx;
312                            y = py - ry;
313                            double prsq = x * x + y * y;
314                            x = px - sx;
315                            y = py - sy;
316                            double pssq = x * x + y * y;
317                            if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) {
318                                double rnoverRS = Math.sqrt((prsq - pnsq) / rssq);
319                                double nx = rx - rnoverRS * b;
320                                double ny = ry + rnoverRS * a;
321                                bestEN = new EastNorth(nx, ny);
322                                bestTime = r.time + rnoverRS * (S.time - r.time);
323                                pnminsq = pnsq;
324                            }
325                        }
326                        r = S;
327                        rx = sx;
328                        ry = sy;
329                    }
330                }
331                if (r != null) {
332                    EastNorth c = r.getEastNorth();
333                    /* if there is only one point in the seg, it will do this twice, but no matter */
334                    rx = c.east();
335                    ry = c.north();
336                    x = px - rx;
337                    y = py - ry;
338                    double prsq = x * x + y * y;
339                    if (prsq < pnminsq) {
340                        pnminsq = prsq;
341                        bestEN = c;
342                        bestTime = r.time;
343                    }
344                }
345            }
346        }
347        if (bestEN == null)
348            return null;
349        WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN));
350        best.time = bestTime;
351        return best;
352    }
353
354    /**
355     * Iterate over all track segments and over all routes.
356     *
357     * @param trackVisibility An array indicating which tracks should be
358     * included in the iteration. Can be null, then all tracks are included.
359     * @return an Iterable object, which iterates over all track segments and
360     * over all routes
361     */
362    public Iterable<Collection<WayPoint>> getLinesIterable(final boolean ... trackVisibility) {
363        return () -> new LinesIterator(this, trackVisibility);
364    }
365
366    /**
367     * Resets the internal caches of east/north coordinates.
368     */
369    public void resetEastNorthCache() {
370        if (waypoints != null) {
371            for (WayPoint wp : waypoints) {
372                wp.invalidateEastNorthCache();
373            }
374        }
375        if (tracks != null) {
376            for (GpxTrack track: tracks) {
377                for (GpxTrackSegment segment: track.getSegments()) {
378                    for (WayPoint wp: segment.getWayPoints()) {
379                        wp.invalidateEastNorthCache();
380                    }
381                }
382            }
383        }
384        if (routes != null) {
385            for (GpxRoute route: routes) {
386                if (route.routePoints == null) {
387                    continue;
388                }
389                for (WayPoint wp: route.routePoints) {
390                    wp.invalidateEastNorthCache();
391                }
392            }
393        }
394    }
395
396    /**
397     * Iterates over all track segments and then over all routes.
398     */
399    public static class LinesIterator implements Iterator<Collection<WayPoint>> {
400
401        private Iterator<GpxTrack> itTracks;
402        private int idxTracks;
403        private Iterator<GpxTrackSegment> itTrackSegments;
404        private final Iterator<GpxRoute> itRoutes;
405
406        private Collection<WayPoint> next;
407        private final boolean[] trackVisibility;
408
409        /**
410         * Constructs a new {@code LinesIterator}.
411         * @param data GPX data
412         * @param trackVisibility An array indicating which tracks should be
413         * included in the iteration. Can be null, then all tracks are included.
414         */
415        public LinesIterator(GpxData data, boolean ... trackVisibility) {
416            itTracks = data.tracks.iterator();
417            idxTracks = -1;
418            itRoutes = data.routes.iterator();
419            this.trackVisibility = trackVisibility;
420            next = getNext();
421        }
422
423        @Override
424        public boolean hasNext() {
425            return next != null;
426        }
427
428        @Override
429        public Collection<WayPoint> next() {
430            if (!hasNext()) {
431                throw new NoSuchElementException();
432            }
433            Collection<WayPoint> current = next;
434            next = getNext();
435            return current;
436        }
437
438        private Collection<WayPoint> getNext() {
439            if (itTracks != null) {
440                if (itTrackSegments != null && itTrackSegments.hasNext()) {
441                    return itTrackSegments.next().getWayPoints();
442                } else {
443                    while (itTracks.hasNext()) {
444                        GpxTrack nxtTrack = itTracks.next();
445                        idxTracks++;
446                        if (trackVisibility != null && !trackVisibility[idxTracks])
447                            continue;
448                        itTrackSegments = nxtTrack.getSegments().iterator();
449                        if (itTrackSegments.hasNext()) {
450                            return itTrackSegments.next().getWayPoints();
451                        }
452                    }
453                    // if we get here, all the Tracks are finished; Continue with Routes
454                    itTracks = null;
455                }
456            }
457            if (itRoutes.hasNext()) {
458                return itRoutes.next().routePoints;
459            }
460            return null;
461        }
462
463        @Override
464        public void remove() {
465            throw new UnsupportedOperationException();
466        }
467    }
468
469    @Override
470    public Collection<DataSource> getDataSources() {
471        return Collections.unmodifiableCollection(dataSources);
472    }
473
474    @Override
475    public int hashCode() {
476        final int prime = 31;
477        int result = 1;
478        result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode());
479        result = prime * result + ((routes == null) ? 0 : routes.hashCode());
480        result = prime * result + ((tracks == null) ? 0 : tracks.hashCode());
481        result = prime * result + ((waypoints == null) ? 0 : waypoints.hashCode());
482        return result;
483    }
484
485    @Override
486    public boolean equals(Object obj) {
487        if (this == obj)
488            return true;
489        if (obj == null)
490            return false;
491        if (getClass() != obj.getClass())
492            return false;
493        GpxData other = (GpxData) obj;
494        if (dataSources == null) {
495            if (other.dataSources != null)
496                return false;
497        } else if (!dataSources.equals(other.dataSources))
498            return false;
499        if (routes == null) {
500            if (other.routes != null)
501                return false;
502        } else if (!routes.equals(other.routes))
503            return false;
504        if (tracks == null) {
505            if (other.tracks != null)
506                return false;
507        } else if (!tracks.equals(other.tracks))
508            return false;
509        if (waypoints == null) {
510            if (other.waypoints != null)
511                return false;
512        } else if (!waypoints.equals(other.waypoints))
513            return false;
514        return true;
515    }
516}