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