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