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