// 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.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;
public String creator;
public final Collection tracks = new LinkedList<>();
public final Collection routes = new LinkedList<>();
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<>();
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 c = S.getEastNorth();
if (R == null) {
R = S;
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;
}
} else {
sx = c.east();
sy = c.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);
}
};
}
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;
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() {
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);
}
}