// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.gpx; import java.awt.geom.Area; import java.io.File; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.Data; import org.openstreetmap.josm.data.DataSource; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.tools.Utils; /** * Objects of this class represent a gpx file with tracks, waypoints and routes. * It uses GPX v1.1, see the spec * for details. * * @author Raphael Mack <ramack@raphael-mack.de> */ public class GpxData extends WithAttributes implements Data { public File storageFile; public boolean fromServer; /** Creator (usually software) */ public String creator; /** Tracks */ public final Collection tracks = new LinkedList<>(); /** Routes */ public final Collection routes = new LinkedList<>(); /** Waypoints */ public final Collection waypoints = new LinkedList<>(); /** * All data sources (bounds of downloaded bounds) of this GpxData.
* Not part of GPX standard but rather a JOSM extension, needed by the fact that * OSM API does not provide {@code } element in its GPX reply. * @since 7575 */ public final Set dataSources = new HashSet<>(); /** * Merges data from another object. * @param other existing GPX data */ public void mergeFrom(GpxData other) { if (storageFile == null && other.storageFile != null) { storageFile = other.storageFile; } fromServer = fromServer && other.fromServer; for (Map.Entry ent : other.attr.entrySet()) { // TODO: Detect conflicts. String k = ent.getKey(); if (META_LINKS.equals(k) && attr.containsKey(META_LINKS)) { Collection my = super.getCollection(META_LINKS); @SuppressWarnings("unchecked") Collection their = (Collection) ent.getValue(); my.addAll(their); } else { put(k, ent.getValue()); } } tracks.addAll(other.tracks); routes.addAll(other.routes); waypoints.addAll(other.waypoints); dataSources.addAll(other.dataSources); } /** * Determines if this GPX data has one or more track points * @return {@code true} if this GPX data has track points, {@code false} otherwise */ public boolean hasTrackPoints() { for (GpxTrack trk : tracks) { for (GpxTrackSegment trkseg : trk.getSegments()) { if (!trkseg.getWayPoints().isEmpty()) return true; } } return false; } /** * Determines if this GPX data has one or more route points * @return {@code true} if this GPX data has route points, {@code false} otherwise */ public boolean hasRoutePoints() { for (GpxRoute rte : routes) { if (!rte.routePoints.isEmpty()) return true; } return false; } /** * Determines if this GPX data is empty (i.e. does not contain any point) * @return {@code true} if this GPX data is empty, {@code false} otherwise */ public boolean isEmpty() { return !hasRoutePoints() && !hasTrackPoints() && waypoints.isEmpty(); } /** * Returns the bounds defining the extend of this data, as read in metadata, if any. * If no bounds is defined in metadata, {@code null} is returned. There is no guarantee * that data entirely fit in this bounds, as it is not recalculated. To get recalculated bounds, * see {@link #recalculateBounds()}. To get downloaded areas, see {@link #dataSources}. * @return the bounds defining the extend of this data, or {@code null}. * @see #recalculateBounds() * @see #dataSources * @since 7575 */ public Bounds getMetaBounds() { Object value = get(META_BOUNDS); if (value instanceof Bounds) { return (Bounds) value; } return null; } /** * Calculates the bounding box of available data and returns it. * The bounds are not stored internally, but recalculated every time * this function is called.
* To get bounds as read from metadata, see {@link #getMetaBounds()}.
* To get downloaded areas, see {@link #dataSources}.
* * FIXME might perhaps use visitor pattern? * @return the bounds * @see #getMetaBounds() * @see #dataSources */ public Bounds recalculateBounds() { Bounds bounds = null; for (WayPoint wpt : waypoints) { if (bounds == null) { bounds = new Bounds(wpt.getCoor()); } else { bounds.extend(wpt.getCoor()); } } for (GpxRoute rte : routes) { for (WayPoint wpt : rte.routePoints) { if (bounds == null) { bounds = new Bounds(wpt.getCoor()); } else { bounds.extend(wpt.getCoor()); } } } for (GpxTrack trk : tracks) { Bounds trkBounds = trk.getBounds(); if (trkBounds != null) { if (bounds == null) { bounds = new Bounds(trkBounds); } else { bounds.extend(trkBounds); } } } return bounds; } /** * calculates the sum of the lengths of all track segments * @return the length in meters */ public double length() { double result = 0.0; // in meters for (GpxTrack trk : tracks) { result += trk.length(); } return result; } /** * returns minimum and maximum timestamps in the track * @param trk track to analyze * @return minimum and maximum dates in array of 2 elements */ public static Date[] getMinMaxTimeForTrack(GpxTrack trk) { WayPoint earliest = null, latest = null; for (GpxTrackSegment seg : trk.getSegments()) { for (WayPoint pnt : seg.getWayPoints()) { if (latest == null) { latest = earliest = pnt; } else { if (pnt.compareTo(earliest) < 0) { earliest = pnt; } else { latest = pnt; } } } } if (earliest == null || latest == null) return null; return new Date[]{earliest.getTime(), latest.getTime()}; } /** * Returns minimum and maximum timestamps for all tracks * Warning: there are lot of track with broken timestamps, * so we just ingore points from future and from year before 1970 in this method * works correctly @since 5815 * @return minimum and maximum dates in array of 2 elements */ public Date[] getMinMaxTimeForAllTracks() { double min = 1e100; double max = -1e100; double now = System.currentTimeMillis()/1000.0; for (GpxTrack trk: tracks) { for (GpxTrackSegment seg : trk.getSegments()) { for (WayPoint pnt : seg.getWayPoints()) { double t = pnt.time; if (t > 0 && t <= now) { if (t > max) max = t; if (t < min) min = t; } } } } if (Utils.equalsEpsilon(min, 1e100) || Utils.equalsEpsilon(max, -1e100)) return new Date[0]; return new Date[]{new Date((long) (min * 1000)), new Date((long) (max * 1000))}; } /** * Makes a WayPoint at the projection of point p onto the track providing p is less than * tolerance away from the track * * @param p : the point to determine the projection for * @param tolerance : must be no further than this from the track * @return the closest point on the track to p, which may be the first or last point if off the * end of a segment, or may be null if nothing close enough */ public WayPoint nearestPointOnTrack(EastNorth p, double tolerance) { /* * assume the coordinates of P are xp,yp, and those of a section of track between two * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point. * * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr * * Also, note that the distance RS^2 is A^2 + B^2 * * If RS^2 == 0.0 ignore the degenerate section of track * * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line * * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 - * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2 * * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2 * * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A * * where RN = sqrt(PR^2 - PN^2) */ double pnminsq = tolerance * tolerance; EastNorth bestEN = null; double bestTime = 0.0; double px = p.east(); double py = p.north(); double rx = 0.0, ry = 0.0, sx, sy, x, y; if (tracks == null) return null; for (GpxTrack track : tracks) { for (GpxTrackSegment seg : track.getSegments()) { WayPoint r = null; for (WayPoint S : seg.getWayPoints()) { EastNorth en = S.getEastNorth(); if (r == null) { r = S; rx = en.east(); ry = en.north(); x = px - rx; y = py - ry; double pRsq = x * x + y * y; if (pRsq < pnminsq) { pnminsq = pRsq; bestEN = en; bestTime = r.time; } } else { sx = en.east(); sy = en.north(); double a = sy - ry; double b = rx - sx; double c = -a * rx - b * ry; double rssq = a * a + b * b; if (rssq == 0) { continue; } double pnsq = a * px + b * py + c; pnsq = pnsq * pnsq / rssq; if (pnsq < pnminsq) { x = px - rx; y = py - ry; double prsq = x * x + y * y; x = px - sx; y = py - sy; double pssq = x * x + y * y; if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) { double rnoverRS = Math.sqrt((prsq - pnsq) / rssq); double nx = rx - rnoverRS * b; double ny = ry + rnoverRS * a; bestEN = new EastNorth(nx, ny); bestTime = r.time + rnoverRS * (S.time - r.time); pnminsq = pnsq; } } r = S; rx = sx; ry = sy; } } if (r != null) { EastNorth c = r.getEastNorth(); /* if there is only one point in the seg, it will do this twice, but no matter */ rx = c.east(); ry = c.north(); x = px - rx; y = py - ry; double prsq = x * x + y * y; if (prsq < pnminsq) { pnminsq = prsq; bestEN = c; bestTime = r.time; } } } } if (bestEN == null) return null; WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN)); best.time = bestTime; return best; } /** * Iterate over all track segments and over all routes. * * @param trackVisibility An array indicating which tracks should be * included in the iteration. Can be null, then all tracks are included. * @return an Iterable object, which iterates over all track segments and * over all routes */ public Iterable> getLinesIterable(final boolean[] trackVisibility) { return new Iterable>() { @Override public Iterator> iterator() { return new LinesIterator(GpxData.this, trackVisibility); } }; } /** * Resets the internal caches of east/north coordinates. */ public void resetEastNorthCache() { if (waypoints != null) { for (WayPoint wp : waypoints) { wp.invalidateEastNorthCache(); } } if (tracks != null) { for (GpxTrack track: tracks) { for (GpxTrackSegment segment: track.getSegments()) { for (WayPoint wp: segment.getWayPoints()) { wp.invalidateEastNorthCache(); } } } } if (routes != null) { for (GpxRoute route: routes) { if (route.routePoints == null) { continue; } for (WayPoint wp: route.routePoints) { wp.invalidateEastNorthCache(); } } } } /** * Iterates over all track segments and then over all routes. */ public static class LinesIterator implements Iterator> { private Iterator itTracks; private int idxTracks; private Iterator itTrackSegments; private final Iterator itRoutes; private Collection next; private final boolean[] trackVisibility; /** * Constructs a new {@code LinesIterator}. * @param data GPX data * @param trackVisibility An array indicating which tracks should be * included in the iteration. Can be null, then all tracks are included. */ public LinesIterator(GpxData data, boolean[] trackVisibility) { itTracks = data.tracks.iterator(); idxTracks = -1; itRoutes = data.routes.iterator(); this.trackVisibility = trackVisibility; next = getNext(); } @Override public boolean hasNext() { return next != null; } @Override public Collection next() { if (!hasNext()) { throw new NoSuchElementException(); } Collection current = next; next = getNext(); return current; } private Collection getNext() { if (itTracks != null) { if (itTrackSegments != null && itTrackSegments.hasNext()) { return itTrackSegments.next().getWayPoints(); } else { while (itTracks.hasNext()) { GpxTrack nxtTrack = itTracks.next(); idxTracks++; if (trackVisibility != null && !trackVisibility[idxTracks]) continue; itTrackSegments = nxtTrack.getSegments().iterator(); if (itTrackSegments.hasNext()) { return itTrackSegments.next().getWayPoints(); } } // if we get here, all the Tracks are finished; Continue with Routes itTracks = null; } } if (itRoutes.hasNext()) { return itRoutes.next().routePoints; } return null; } @Override public void remove() { throw new UnsupportedOperationException(); } } @Override public Collection getDataSources() { return dataSources; } @Override public Area getDataSourceArea() { return DataSource.getDataSourceArea(dataSources); } @Override public List getDataSourceBounds() { return DataSource.getDataSourceBounds(dataSources); } }