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