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