// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.gpx; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.DoubleSummaryStatistics; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; 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; /** * Objects of this class represent a gpx file with tracks, waypoints and routes. * It uses GPX v1.1, see <a href="http://www.topografix.com/GPX/1/1/">the spec</a> * 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<GpxTrack> tracks = new LinkedList<>(); /** Routes */ public final Collection<GpxRoute> routes = new LinkedList<>(); /** Waypoints */ public final Collection<WayPoint> waypoints = new LinkedList<>(); /** * All data sources (bounds of downloaded bounds) of this GpxData.<br> * Not part of GPX standard but rather a JOSM extension, needed by the fact that * OSM API does not provide {@code <bounds>} element in its GPX reply. * @since 7575 */ public final Set<DataSource> 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<String, Object> ent : other.attr.entrySet()) { // TODO: Detect conflicts. String k = ent.getKey(); if (META_LINKS.equals(k) && attr.containsKey(META_LINKS)) { Collection<GpxLink> my = super.<GpxLink>getCollection(META_LINKS); @SuppressWarnings("unchecked") Collection<GpxLink> their = (Collection<GpxLink>) 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.<br> * To get bounds as read from metadata, see {@link #getMetaBounds()}.<br> * To get downloaded areas, see {@link #dataSources}.<br> * * 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) { final DoubleSummaryStatistics statistics = trk.getSegments().stream() .flatMap(seg -> seg.getWayPoints().stream()) .mapToDouble(pnt -> pnt.time) .summaryStatistics(); return statistics.getCount() == 0 ? null : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 1000))}; } /** * 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 now = System.currentTimeMillis() / 1000.0; final DoubleSummaryStatistics statistics = tracks.stream() .flatMap(trk -> trk.getSegments().stream()) .flatMap(seg -> seg.getWayPoints().stream()) .mapToDouble(pnt -> pnt.time) .filter(t -> t > 0 && t <= now) .summaryStatistics(); return statistics.getCount() == 0 ? new Date[0] : new Date[]{new Date((long) (statistics.getMin() * 1000)), new Date((long) (statistics.getMax() * 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<Collection<WayPoint>> getLinesIterable(final boolean... trackVisibility) { return () -> new LinesIterator(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<Collection<WayPoint>> { private Iterator<GpxTrack> itTracks; private int idxTracks; private Iterator<GpxTrackSegment> itTrackSegments; private final Iterator<GpxRoute> itRoutes; private Collection<WayPoint> 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<WayPoint> next() { if (!hasNext()) { throw new NoSuchElementException(); } Collection<WayPoint> current = next; next = getNext(); return current; } private Collection<WayPoint> 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<DataSource> getDataSources() { return Collections.unmodifiableCollection(dataSources); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode()); result = prime * result + ((routes == null) ? 0 : routes.hashCode()); result = prime * result + ((tracks == null) ? 0 : tracks.hashCode()); result = prime * result + ((waypoints == null) ? 0 : waypoints.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; GpxData other = (GpxData) obj; if (dataSources == null) { if (other.dataSources != null) return false; } else if (!dataSources.equals(other.dataSources)) return false; if (routes == null) { if (other.routes != null) return false; } else if (!routes.equals(other.routes)) return false; if (tracks == null) { if (other.tracks != null) return false; } else if (!tracks.equals(other.tracks)) return false; if (waypoints == null) { if (other.waypoints != null) return false; } else if (!waypoints.equals(other.waypoints)) return false; return true; } }