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