001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.gpx; 003 004import java.awt.geom.Area; 005import java.io.File; 006import java.util.Collection; 007import java.util.Date; 008import java.util.HashSet; 009import java.util.Iterator; 010import java.util.LinkedList; 011import java.util.List; 012import java.util.Map; 013import java.util.Set; 014 015import org.openstreetmap.josm.Main; 016import org.openstreetmap.josm.data.Bounds; 017import org.openstreetmap.josm.data.Data; 018import org.openstreetmap.josm.data.DataSource; 019import org.openstreetmap.josm.data.coor.EastNorth; 020import org.openstreetmap.josm.tools.Utils; 021 022/** 023 * Objects of this class represent a gpx file with tracks, waypoints and routes. 024 * It uses GPX v1.1, see <a href="http://www.topografix.com/GPX/1/1/">the spec</a> 025 * for details. 026 * 027 * @author Raphael Mack <ramack@raphael-mack.de> 028 */ 029public class GpxData extends WithAttributes implements Data { 030 031 public File storageFile; 032 public boolean fromServer; 033 034 public String creator; 035 036 public final Collection<GpxTrack> tracks = new LinkedList<>(); 037 public final Collection<GpxRoute> routes = new LinkedList<>(); 038 public final Collection<WayPoint> waypoints = new LinkedList<>(); 039 040 /** 041 * All data sources (bounds of downloaded bounds) of this GpxData.<br> 042 * Not part of GPX standard but rather a JOSM extension, needed by the fact that 043 * OSM API does not provide {@code <bounds>} element in its GPX reply. 044 * @since 7575 045 */ 046 public final Set<DataSource> dataSources = new HashSet<>(); 047 048 public void mergeFrom(GpxData other) { 049 if (storageFile == null && other.storageFile != null) { 050 storageFile = other.storageFile; 051 } 052 fromServer = fromServer && other.fromServer; 053 054 for (Map.Entry<String, Object> ent : other.attr.entrySet()) { 055 // TODO: Detect conflicts. 056 String k = ent.getKey(); 057 if (META_LINKS.equals(k) && attr.containsKey(META_LINKS)) { 058 Collection<GpxLink> my = super.<GpxLink>getCollection(META_LINKS); 059 @SuppressWarnings("unchecked") 060 Collection<GpxLink> their = (Collection<GpxLink>) ent.getValue(); 061 my.addAll(their); 062 } else { 063 put(k, ent.getValue()); 064 } 065 } 066 tracks.addAll(other.tracks); 067 routes.addAll(other.routes); 068 waypoints.addAll(other.waypoints); 069 dataSources.addAll(other.dataSources); 070 } 071 072 /** 073 * Determines if this GPX data has one or more track points 074 * @return {@code true} if this GPX data has track points, {@code false} otherwise 075 */ 076 public boolean hasTrackPoints() { 077 for (GpxTrack trk : tracks) { 078 for (GpxTrackSegment trkseg : trk.getSegments()) { 079 if (!trkseg.getWayPoints().isEmpty()) 080 return true; 081 } 082 } 083 return false; 084 } 085 086 /** 087 * Determines if this GPX data has one or more route points 088 * @return {@code true} if this GPX data has route points, {@code false} otherwise 089 */ 090 public boolean hasRoutePoints() { 091 for (GpxRoute rte : routes) { 092 if (!rte.routePoints.isEmpty()) 093 return true; 094 } 095 return false; 096 } 097 098 /** 099 * Determines if this GPX data is empty (i.e. does not contain any point) 100 * @return {@code true} if this GPX data is empty, {@code false} otherwise 101 */ 102 public boolean isEmpty() { 103 return !hasRoutePoints() && !hasTrackPoints() && waypoints.isEmpty(); 104 } 105 106 /** 107 * Returns the bounds defining the extend of this data, as read in metadata, if any. 108 * If no bounds is defined in metadata, {@code null} is returned. There is no guarantee 109 * that data entirely fit in this bounds, as it is not recalculated. To get recalculated bounds, 110 * see {@link #recalculateBounds()}. To get downloaded areas, see {@link #dataSources}. 111 * @return the bounds defining the extend of this data, or {@code null}. 112 * @see #recalculateBounds() 113 * @see #dataSources 114 * @since 7575 115 */ 116 public Bounds getMetaBounds() { 117 Object value = get(META_BOUNDS); 118 if (value instanceof Bounds) { 119 return (Bounds) value; 120 } 121 return null; 122 } 123 124 /** 125 * Calculates the bounding box of available data and returns it. 126 * The bounds are not stored internally, but recalculated every time 127 * this function is called.<br> 128 * To get bounds as read from metadata, see {@link #getMetaBounds()}.<br> 129 * To get downloaded areas, see {@link #dataSources}.<br> 130 * 131 * FIXME might perhaps use visitor pattern? 132 * @return the bounds 133 * @see #getMetaBounds() 134 * @see #dataSources 135 */ 136 public Bounds recalculateBounds() { 137 Bounds bounds = null; 138 for (WayPoint wpt : waypoints) { 139 if (bounds == null) { 140 bounds = new Bounds(wpt.getCoor()); 141 } else { 142 bounds.extend(wpt.getCoor()); 143 } 144 } 145 for (GpxRoute rte : routes) { 146 for (WayPoint wpt : rte.routePoints) { 147 if (bounds == null) { 148 bounds = new Bounds(wpt.getCoor()); 149 } else { 150 bounds.extend(wpt.getCoor()); 151 } 152 } 153 } 154 for (GpxTrack trk : tracks) { 155 Bounds trkBounds = trk.getBounds(); 156 if (trkBounds != null) { 157 if (bounds == null) { 158 bounds = new Bounds(trkBounds); 159 } else { 160 bounds.extend(trkBounds); 161 } 162 } 163 } 164 return bounds; 165 } 166 167 /** 168 * calculates the sum of the lengths of all track segments 169 * @return the length in meters 170 */ 171 public double length() { 172 double result = 0.0; // in meters 173 174 for (GpxTrack trk : tracks) { 175 result += trk.length(); 176 } 177 178 return result; 179 } 180 181 /** 182 * returns minimum and maximum timestamps in the track 183 * @param trk track to analyze 184 * @return minimum and maximum dates in array of 2 elements 185 */ 186 public static Date[] getMinMaxTimeForTrack(GpxTrack trk) { 187 WayPoint earliest = null, latest = null; 188 189 for (GpxTrackSegment seg : trk.getSegments()) { 190 for (WayPoint pnt : seg.getWayPoints()) { 191 if (latest == null) { 192 latest = earliest = pnt; 193 } else { 194 if (pnt.compareTo(earliest) < 0) { 195 earliest = pnt; 196 } else { 197 latest = pnt; 198 } 199 } 200 } 201 } 202 if (earliest == null || latest == null) return null; 203 return new Date[]{earliest.getTime(), latest.getTime()}; 204 } 205 206 /** 207 * Returns minimum and maximum timestamps for all tracks 208 * Warning: there are lot of track with broken timestamps, 209 * so we just ingore points from future and from year before 1970 in this method 210 * works correctly @since 5815 211 * @return minimum and maximum dates in array of 2 elements 212 */ 213 public Date[] getMinMaxTimeForAllTracks() { 214 double min = 1e100; 215 double max = -1e100; 216 double now = System.currentTimeMillis()/1000.0; 217 for (GpxTrack trk: tracks) { 218 for (GpxTrackSegment seg : trk.getSegments()) { 219 for (WayPoint pnt : seg.getWayPoints()) { 220 double t = pnt.time; 221 if (t > 0 && t <= now) { 222 if (t > max) max = t; 223 if (t < min) min = t; 224 } 225 } 226 } 227 } 228 if (Utils.equalsEpsilon(min, 1e100) || Utils.equalsEpsilon(max, -1e100)) return new Date[0]; 229 return new Date[]{new Date((long) (min * 1000)), new Date((long) (max * 1000))}; 230 } 231 232 /** 233 * Makes a WayPoint at the projection of point P onto the track providing P is less than 234 * tolerance away from the track 235 * 236 * @param P : the point to determine the projection for 237 * @param tolerance : must be no further than this from the track 238 * @return the closest point on the track to P, which may be the first or last point if off the 239 * end of a segment, or may be null if nothing close enough 240 */ 241 public WayPoint nearestPointOnTrack(EastNorth P, double tolerance) { 242 /* 243 * assume the coordinates of P are xp,yp, and those of a section of track between two 244 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point. 245 * 246 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr 247 * 248 * Also, note that the distance RS^2 is A^2 + B^2 249 * 250 * If RS^2 == 0.0 ignore the degenerate section of track 251 * 252 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line 253 * 254 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line 255 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 - 256 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2 257 * 258 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2 259 * 260 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A 261 * 262 * where RN = sqrt(PR^2 - PN^2) 263 */ 264 265 double PNminsq = tolerance * tolerance; 266 EastNorth bestEN = null; 267 double bestTime = 0.0; 268 double px = P.east(); 269 double py = P.north(); 270 double rx = 0.0, ry = 0.0, sx, sy, x, y; 271 if (tracks == null) 272 return null; 273 for (GpxTrack track : tracks) { 274 for (GpxTrackSegment seg : track.getSegments()) { 275 WayPoint R = null; 276 for (WayPoint S : seg.getWayPoints()) { 277 EastNorth c = S.getEastNorth(); 278 if (R == null) { 279 R = S; 280 rx = c.east(); 281 ry = c.north(); 282 x = px - rx; 283 y = py - ry; 284 double PRsq = x * x + y * y; 285 if (PRsq < PNminsq) { 286 PNminsq = PRsq; 287 bestEN = c; 288 bestTime = R.time; 289 } 290 } else { 291 sx = c.east(); 292 sy = c.north(); 293 double A = sy - ry; 294 double B = rx - sx; 295 double C = -A * rx - B * ry; 296 double RSsq = A * A + B * B; 297 if (RSsq == 0) { 298 continue; 299 } 300 double PNsq = A * px + B * py + C; 301 PNsq = PNsq * PNsq / RSsq; 302 if (PNsq < PNminsq) { 303 x = px - rx; 304 y = py - ry; 305 double PRsq = x * x + y * y; 306 x = px - sx; 307 y = py - sy; 308 double PSsq = x * x + y * y; 309 if (PRsq - PNsq <= RSsq && PSsq - PNsq <= RSsq) { 310 double RNoverRS = Math.sqrt((PRsq - PNsq) / RSsq); 311 double nx = rx - RNoverRS * B; 312 double ny = ry + RNoverRS * A; 313 bestEN = new EastNorth(nx, ny); 314 bestTime = R.time + RNoverRS * (S.time - R.time); 315 PNminsq = PNsq; 316 } 317 } 318 R = S; 319 rx = sx; 320 ry = sy; 321 } 322 } 323 if (R != null) { 324 EastNorth c = R.getEastNorth(); 325 /* if there is only one point in the seg, it will do this twice, but no matter */ 326 rx = c.east(); 327 ry = c.north(); 328 x = px - rx; 329 y = py - ry; 330 double PRsq = x * x + y * y; 331 if (PRsq < PNminsq) { 332 PNminsq = PRsq; 333 bestEN = c; 334 bestTime = R.time; 335 } 336 } 337 } 338 } 339 if (bestEN == null) 340 return null; 341 WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN)); 342 best.time = bestTime; 343 return best; 344 } 345 346 /** 347 * Iterate over all track segments and over all routes. 348 * 349 * @param trackVisibility An array indicating which tracks should be 350 * included in the iteration. Can be null, then all tracks are included. 351 * @return an Iterable object, which iterates over all track segments and 352 * over all routes 353 */ 354 public Iterable<Collection<WayPoint>> getLinesIterable(final boolean[] trackVisibility) { 355 return new Iterable<Collection<WayPoint>>() { 356 @Override 357 public Iterator<Collection<WayPoint>> iterator() { 358 return new LinesIterator(GpxData.this, trackVisibility); 359 } 360 }; 361 } 362 363 public void resetEastNorthCache() { 364 if (waypoints != null) { 365 for (WayPoint wp : waypoints) { 366 wp.invalidateEastNorthCache(); 367 } 368 } 369 if (tracks != null) { 370 for (GpxTrack track: tracks) { 371 for (GpxTrackSegment segment: track.getSegments()) { 372 for (WayPoint wp: segment.getWayPoints()) { 373 wp.invalidateEastNorthCache(); 374 } 375 } 376 } 377 } 378 if (routes != null) { 379 for (GpxRoute route: routes) { 380 if (route.routePoints == null) { 381 continue; 382 } 383 for (WayPoint wp: route.routePoints) { 384 wp.invalidateEastNorthCache(); 385 } 386 } 387 } 388 } 389 390 /** 391 * Iterates over all track segments and then over all routes. 392 */ 393 public static class LinesIterator implements Iterator<Collection<WayPoint>> { 394 395 private Iterator<GpxTrack> itTracks; 396 private int idxTracks; 397 private Iterator<GpxTrackSegment> itTrackSegments; 398 private final Iterator<GpxRoute> itRoutes; 399 400 private Collection<WayPoint> next; 401 private final boolean[] trackVisibility; 402 403 public LinesIterator(GpxData data, boolean[] trackVisibility) { 404 itTracks = data.tracks.iterator(); 405 idxTracks = -1; 406 itRoutes = data.routes.iterator(); 407 this.trackVisibility = trackVisibility; 408 next = getNext(); 409 } 410 411 @Override 412 public boolean hasNext() { 413 return next != null; 414 } 415 416 @Override 417 public Collection<WayPoint> next() { 418 Collection<WayPoint> current = next; 419 next = getNext(); 420 return current; 421 } 422 423 private Collection<WayPoint> getNext() { 424 if (itTracks != null) { 425 if (itTrackSegments != null && itTrackSegments.hasNext()) { 426 return itTrackSegments.next().getWayPoints(); 427 } else { 428 while (itTracks.hasNext()) { 429 GpxTrack nxtTrack = itTracks.next(); 430 idxTracks++; 431 if (trackVisibility != null && !trackVisibility[idxTracks]) 432 continue; 433 itTrackSegments = nxtTrack.getSegments().iterator(); 434 if (itTrackSegments.hasNext()) { 435 return itTrackSegments.next().getWayPoints(); 436 } 437 } 438 // if we get here, all the Tracks are finished; Continue with Routes 439 itTracks = null; 440 } 441 } 442 if (itRoutes.hasNext()) { 443 return itRoutes.next().routePoints; 444 } 445 return null; 446 } 447 448 @Override 449 public void remove() { 450 throw new UnsupportedOperationException(); 451 } 452 } 453 454 @Override 455 public Collection<DataSource> getDataSources() { 456 return dataSources; 457 } 458 459 @Override 460 public Area getDataSourceArea() { 461 return DataSource.getDataSourceArea(dataSources); 462 } 463 464 @Override 465 public List<Bounds> getDataSourceBounds() { 466 return DataSource.getDataSourceBounds(dataSources); 467 } 468}