// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.gpx;
import java.io.File;
import java.text.MessageFormat;
import java.util.AbstractCollection;
import java.util.ArrayList;
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.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.Stream;
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.data.gpx.GpxTrack.GpxTrackChangeListener;
import org.openstreetmap.josm.tools.ListenerList;
/**
* 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 {
/**
* The disk file this layer is stored in, if it is a local layer. May be null
.
*/
public File storageFile;
/**
* A boolean flag indicating if the data was read from the OSM server.
*/
public boolean fromServer;
/**
* Creator metadata for this file (usually software)
*/
public String creator;
/**
* A list of tracks this file consists of
*/
private final ArrayList privateTracks = new ArrayList<>();
/**
* GXP routes in this file
*/
private final ArrayList privateRoutes = new ArrayList<>();
/**
* Addidionaly waypoints for this file.
*/
private final ArrayList privateWaypoints = new ArrayList<>();
private final GpxTrackChangeListener proxy = e -> fireInvalidate();
/**
* Tracks. Access is discouraged, use {@link #getTracks()} to read.
* @see #getTracks()
*/
public final Collection tracks = new ListeningCollection(privateTracks, this::fireInvalidate) {
@Override
protected void removed(GpxTrack cursor) {
cursor.removeListener(proxy);
super.removed(cursor);
}
@Override
protected void added(GpxTrack cursor) {
super.added(cursor);
cursor.addListener(proxy);
}
};
/**
* Routes. Access is discouraged, use {@link #getTracks()} to read.
* @see #getRoutes()
*/
public final Collection routes = new ListeningCollection<>(privateRoutes, this::fireInvalidate);
/**
* Waypoints. Access is discouraged, use {@link #getTracks()} to read.
* @see #getWaypoints()
*/
public final Collection waypoints = new ListeningCollection<>(privateWaypoints, this::fireInvalidate);
/**
* 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<>();
private final ListenerList listeners = ListenerList.create();
/**
* 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());
}
}
other.getTracks().forEach(this::addTrack);
other.getRoutes().forEach(this::addRoute);
other.getWaypoints().forEach(this::addWaypoint);
dataSources.addAll(other.dataSources);
fireInvalidate();
}
/**
* Get all tracks contained in this data set.
* @return The tracks.
*/
public Collection getTracks() {
return Collections.unmodifiableCollection(privateTracks);
}
/**
* Add a new track
* @param track The new track
* @since 12156
*/
public void addTrack(GpxTrack track) {
if (privateTracks.stream().anyMatch(t -> t == track)) {
throw new IllegalArgumentException(MessageFormat.format("The track was already added to this data: {0}", track));
}
privateTracks.add(track);
track.addListener(proxy);
fireInvalidate();
}
/**
* Remove a track
* @param track The old track
* @since 12156
*/
public void removeTrack(GpxTrack track) {
if (!privateTracks.removeIf(t -> t == track)) {
throw new IllegalArgumentException(MessageFormat.format("The track was not in this data: {0}", track));
}
track.removeListener(proxy);
fireInvalidate();
}
/**
* Gets the list of all routes defined in this data set.
* @return The routes
* @since 12156
*/
public Collection getRoutes() {
return Collections.unmodifiableCollection(privateRoutes);
}
/**
* Add a new route
* @param route The new route
* @since 12156
*/
public void addRoute(GpxRoute route) {
if (privateRoutes.stream().anyMatch(r -> r == route)) {
throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", route));
}
privateRoutes.add(route);
fireInvalidate();
}
/**
* Remove a route
* @param route The old route
* @since 12156
*/
public void removeRoute(GpxRoute route) {
if (!privateRoutes.removeIf(r -> r == route)) {
throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", route));
}
fireInvalidate();
}
/**
* Gets a list of all way points in this data set.
* @return The way points.
* @since 12156
*/
public Collection getWaypoints() {
return Collections.unmodifiableCollection(privateWaypoints);
}
/**
* Add a new waypoint
* @param waypoint The new waypoint
* @since 12156
*/
public void addWaypoint(WayPoint waypoint) {
if (privateWaypoints.stream().anyMatch(w -> w == waypoint)) {
throw new IllegalArgumentException(MessageFormat.format("The route was already added to this data: {0}", waypoint));
}
privateWaypoints.add(waypoint);
fireInvalidate();
}
/**
* Remove a waypoint
* @param waypoint The old waypoint
* @since 12156
*/
public void removeWaypoint(WayPoint waypoint) {
if (!privateWaypoints.removeIf(w -> w == waypoint)) {
throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", waypoint));
}
fireInvalidate();
}
/**
* 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() {
return getTrackPoints().findAny().isPresent();
}
/**
* Gets a stream of all track points in the segments of the tracks of this data.
* @return The stream
* @see #getTracks()
* @see GpxTrack#getSegments()
* @see GpxTrackSegment#getWayPoints()
* @since 12156
*/
public Stream getTrackPoints() {
return getTracks().stream().flatMap(trk -> trk.getSegments().stream()).flatMap(trkseg -> trkseg.getWayPoints().stream());
}
/**
* 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() {
return getRoutes().stream().anyMatch(rte -> !rte.routePoints.isEmpty());
}
/**
* 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 : getWaypoints()) {
if (bounds == null) {
bounds = new Bounds(wpt.getCoor());
} else {
bounds.extend(wpt.getCoor());
}
}
for (GpxRoute rte : getRoutes()) {
for (WayPoint wpt : rte.routePoints) {
if (bounds == null) {
bounds = new Bounds(wpt.getCoor());
} else {
bounds.extend(wpt.getCoor());
}
}
}
for (GpxTrack trk : getTracks()) {
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() {
return getTracks().stream().mapToDouble(GpxTrack::length).sum();
}
/**
* 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;
for (GpxTrack track : getTracks()) {
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 LinesIterator(this, trackVisibility);
}
/**
* Resets the internal caches of east/north coordinates.
*/
public void resetEastNorthCache() {
getWaypoints().forEach(WayPoint::invalidateEastNorthCache);
getTrackPoints().forEach(WayPoint::invalidateEastNorthCache);
for (GpxRoute route: getRoutes()) {
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 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 + ((privateRoutes == null) ? 0 : privateRoutes.hashCode());
result = prime * result + ((privateTracks == null) ? 0 : privateTracks.hashCode());
result = prime * result + ((privateWaypoints == null) ? 0 : privateWaypoints.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 (privateRoutes == null) {
if (other.privateRoutes != null)
return false;
} else if (!privateRoutes.equals(other.privateRoutes))
return false;
if (privateTracks == null) {
if (other.privateTracks != null)
return false;
} else if (!privateTracks.equals(other.privateTracks))
return false;
if (privateWaypoints == null) {
if (other.privateWaypoints != null)
return false;
} else if (!privateWaypoints.equals(other.privateWaypoints))
return false;
return true;
}
/**
* Adds a listener that gets called whenever the data changed.
* @param listener The listener
* @since 12156
*/
public void addChangeListener(GpxDataChangeListener listener) {
listeners.addListener(listener);
}
/**
* Adds a listener that gets called whenever the data changed. It is added with a weak link
* @param listener The listener
*/
public void addWeakChangeListener(GpxDataChangeListener listener) {
listeners.addWeakListener(listener);
}
/**
* Removes a listener that gets called whenever the data changed.
* @param listener The listener
* @since 12156
*/
public void removeChangeListener(GpxDataChangeListener listener) {
listeners.removeListener(listener);
}
private void fireInvalidate() {
GpxDataChangeEvent e = new GpxDataChangeEvent(this);
listeners.fireEvent(l -> l.gpxDataChanged(e));
}
/**
* This is a proxy of a collection that notifies a listener on every collection change
* @author Michael Zangl
*
* @param The entry type
* @since 12156
*/
private static class ListeningCollection extends AbstractCollection {
private final ArrayList base;
private final Runnable runOnModification;
ListeningCollection(ArrayList base, Runnable runOnModification) {
this.base = base;
this.runOnModification = runOnModification;
}
@Override
public Iterator iterator() {
Iterator it = base.iterator();
return new Iterator() {
private T cursor;
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public T next() {
cursor = it.next();
return cursor;
}
@Override
public void remove() {
if (cursor != null) {
removed(cursor);
cursor = null;
}
it.remove();
}
};
}
@Override
public int size() {
return base.size();
}
@Override
@SuppressWarnings("unchecked")
public boolean remove(Object o) {
boolean remove = base.remove(o);
if (remove) {
removed((T) o);
}
return remove;
}
@Override
public boolean add(T e) {
boolean add = base.add(e);
added(e);
return add;
}
protected void removed(T cursor) {
runOnModification.run();
}
protected void added(T cursor) {
runOnModification.run();
}
}
/**
* A listener that listens to GPX data changes.
* @author Michael Zangl
* @since 12156
*/
@FunctionalInterface
public interface GpxDataChangeListener {
/**
* Called when the gpx data changed.
* @param e The event
*/
void gpxDataChanged(GpxDataChangeEvent e);
}
/**
* A data change event in any of the gpx data.
* @author Michael Zangl
* @since 12156
*/
public static class GpxDataChangeEvent {
private final GpxData source;
GpxDataChangeEvent(GpxData source) {
super();
this.source = source;
}
/**
* Get the data that was changed.
* @return The data.
*/
public GpxData getSource() {
return source;
}
}
}