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.HashSet; 009import java.util.Iterator; 010import java.util.LinkedList; 011import java.util.Map; 012import java.util.NoSuchElementException; 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 /** 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 WayPoint earliest = null, latest = null; 196 197 for (GpxTrackSegment seg : trk.getSegments()) { 198 for (WayPoint pnt : seg.getWayPoints()) { 199 if (latest == null) { 200 latest = earliest = pnt; 201 } else { 202 if (pnt.compareTo(earliest) < 0) { 203 earliest = pnt; 204 } else if (pnt.compareTo(latest) > 0) { 205 latest = pnt; 206 } 207 } 208 } 209 } 210 if (earliest == null || latest == null) return null; 211 return new Date[]{earliest.getTime(), latest.getTime()}; 212 } 213 214 /** 215 * Returns minimum and maximum timestamps for all tracks 216 * Warning: there are lot of track with broken timestamps, 217 * so we just ingore points from future and from year before 1970 in this method 218 * works correctly @since 5815 219 * @return minimum and maximum dates in array of 2 elements 220 */ 221 public Date[] getMinMaxTimeForAllTracks() { 222 double min = 1e100; 223 double max = -1e100; 224 double now = System.currentTimeMillis()/1000.0; 225 for (GpxTrack trk: tracks) { 226 for (GpxTrackSegment seg : trk.getSegments()) { 227 for (WayPoint pnt : seg.getWayPoints()) { 228 double t = pnt.time; 229 if (t > 0 && t <= now) { 230 if (t > max) max = t; 231 if (t < min) min = t; 232 } 233 } 234 } 235 } 236 if (Utils.equalsEpsilon(min, 1e100) || Utils.equalsEpsilon(max, -1e100)) return new Date[0]; 237 return new Date[]{new Date((long) (min * 1000)), new Date((long) (max * 1000))}; 238 } 239 240 /** 241 * Makes a WayPoint at the projection of point p onto the track providing p is less than 242 * tolerance away from the track 243 * 244 * @param p : the point to determine the projection for 245 * @param tolerance : must be no further than this from the track 246 * @return the closest point on the track to p, which may be the first or last point if off the 247 * end of a segment, or may be null if nothing close enough 248 */ 249 public WayPoint nearestPointOnTrack(EastNorth p, double tolerance) { 250 /* 251 * assume the coordinates of P are xp,yp, and those of a section of track between two 252 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point. 253 * 254 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr 255 * 256 * Also, note that the distance RS^2 is A^2 + B^2 257 * 258 * If RS^2 == 0.0 ignore the degenerate section of track 259 * 260 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line 261 * 262 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line 263 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 - 264 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2 265 * 266 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2 267 * 268 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A 269 * 270 * where RN = sqrt(PR^2 - PN^2) 271 */ 272 273 double pnminsq = tolerance * tolerance; 274 EastNorth bestEN = null; 275 double bestTime = 0.0; 276 double px = p.east(); 277 double py = p.north(); 278 double rx = 0.0, ry = 0.0, sx, sy, x, y; 279 if (tracks == null) 280 return null; 281 for (GpxTrack track : tracks) { 282 for (GpxTrackSegment seg : track.getSegments()) { 283 WayPoint r = null; 284 for (WayPoint S : seg.getWayPoints()) { 285 EastNorth en = S.getEastNorth(); 286 if (r == null) { 287 r = S; 288 rx = en.east(); 289 ry = en.north(); 290 x = px - rx; 291 y = py - ry; 292 double pRsq = x * x + y * y; 293 if (pRsq < pnminsq) { 294 pnminsq = pRsq; 295 bestEN = en; 296 bestTime = r.time; 297 } 298 } else { 299 sx = en.east(); 300 sy = en.north(); 301 double a = sy - ry; 302 double b = rx - sx; 303 double c = -a * rx - b * ry; 304 double rssq = a * a + b * b; 305 if (rssq == 0) { 306 continue; 307 } 308 double pnsq = a * px + b * py + c; 309 pnsq = pnsq * pnsq / rssq; 310 if (pnsq < pnminsq) { 311 x = px - rx; 312 y = py - ry; 313 double prsq = x * x + y * y; 314 x = px - sx; 315 y = py - sy; 316 double pssq = x * x + y * y; 317 if (prsq - pnsq <= rssq && pssq - pnsq <= rssq) { 318 double rnoverRS = Math.sqrt((prsq - pnsq) / rssq); 319 double nx = rx - rnoverRS * b; 320 double ny = ry + rnoverRS * a; 321 bestEN = new EastNorth(nx, ny); 322 bestTime = r.time + rnoverRS * (S.time - r.time); 323 pnminsq = pnsq; 324 } 325 } 326 r = S; 327 rx = sx; 328 ry = sy; 329 } 330 } 331 if (r != null) { 332 EastNorth c = r.getEastNorth(); 333 /* if there is only one point in the seg, it will do this twice, but no matter */ 334 rx = c.east(); 335 ry = c.north(); 336 x = px - rx; 337 y = py - ry; 338 double prsq = x * x + y * y; 339 if (prsq < pnminsq) { 340 pnminsq = prsq; 341 bestEN = c; 342 bestTime = r.time; 343 } 344 } 345 } 346 } 347 if (bestEN == null) 348 return null; 349 WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN)); 350 best.time = bestTime; 351 return best; 352 } 353 354 /** 355 * Iterate over all track segments and over all routes. 356 * 357 * @param trackVisibility An array indicating which tracks should be 358 * included in the iteration. Can be null, then all tracks are included. 359 * @return an Iterable object, which iterates over all track segments and 360 * over all routes 361 */ 362 public Iterable<Collection<WayPoint>> getLinesIterable(final boolean ... trackVisibility) { 363 return () -> new LinesIterator(this, trackVisibility); 364 } 365 366 /** 367 * Resets the internal caches of east/north coordinates. 368 */ 369 public void resetEastNorthCache() { 370 if (waypoints != null) { 371 for (WayPoint wp : waypoints) { 372 wp.invalidateEastNorthCache(); 373 } 374 } 375 if (tracks != null) { 376 for (GpxTrack track: tracks) { 377 for (GpxTrackSegment segment: track.getSegments()) { 378 for (WayPoint wp: segment.getWayPoints()) { 379 wp.invalidateEastNorthCache(); 380 } 381 } 382 } 383 } 384 if (routes != null) { 385 for (GpxRoute route: routes) { 386 if (route.routePoints == null) { 387 continue; 388 } 389 for (WayPoint wp: route.routePoints) { 390 wp.invalidateEastNorthCache(); 391 } 392 } 393 } 394 } 395 396 /** 397 * Iterates over all track segments and then over all routes. 398 */ 399 public static class LinesIterator implements Iterator<Collection<WayPoint>> { 400 401 private Iterator<GpxTrack> itTracks; 402 private int idxTracks; 403 private Iterator<GpxTrackSegment> itTrackSegments; 404 private final Iterator<GpxRoute> itRoutes; 405 406 private Collection<WayPoint> next; 407 private final boolean[] trackVisibility; 408 409 /** 410 * Constructs a new {@code LinesIterator}. 411 * @param data GPX data 412 * @param trackVisibility An array indicating which tracks should be 413 * included in the iteration. Can be null, then all tracks are included. 414 */ 415 public LinesIterator(GpxData data, boolean ... trackVisibility) { 416 itTracks = data.tracks.iterator(); 417 idxTracks = -1; 418 itRoutes = data.routes.iterator(); 419 this.trackVisibility = trackVisibility; 420 next = getNext(); 421 } 422 423 @Override 424 public boolean hasNext() { 425 return next != null; 426 } 427 428 @Override 429 public Collection<WayPoint> next() { 430 if (!hasNext()) { 431 throw new NoSuchElementException(); 432 } 433 Collection<WayPoint> current = next; 434 next = getNext(); 435 return current; 436 } 437 438 private Collection<WayPoint> getNext() { 439 if (itTracks != null) { 440 if (itTrackSegments != null && itTrackSegments.hasNext()) { 441 return itTrackSegments.next().getWayPoints(); 442 } else { 443 while (itTracks.hasNext()) { 444 GpxTrack nxtTrack = itTracks.next(); 445 idxTracks++; 446 if (trackVisibility != null && !trackVisibility[idxTracks]) 447 continue; 448 itTrackSegments = nxtTrack.getSegments().iterator(); 449 if (itTrackSegments.hasNext()) { 450 return itTrackSegments.next().getWayPoints(); 451 } 452 } 453 // if we get here, all the Tracks are finished; Continue with Routes 454 itTracks = null; 455 } 456 } 457 if (itRoutes.hasNext()) { 458 return itRoutes.next().routePoints; 459 } 460 return null; 461 } 462 463 @Override 464 public void remove() { 465 throw new UnsupportedOperationException(); 466 } 467 } 468 469 @Override 470 public Collection<DataSource> getDataSources() { 471 return Collections.unmodifiableCollection(dataSources); 472 } 473 474 @Override 475 public int hashCode() { 476 final int prime = 31; 477 int result = 1; 478 result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode()); 479 result = prime * result + ((routes == null) ? 0 : routes.hashCode()); 480 result = prime * result + ((tracks == null) ? 0 : tracks.hashCode()); 481 result = prime * result + ((waypoints == null) ? 0 : waypoints.hashCode()); 482 return result; 483 } 484 485 @Override 486 public boolean equals(Object obj) { 487 if (this == obj) 488 return true; 489 if (obj == null) 490 return false; 491 if (getClass() != obj.getClass()) 492 return false; 493 GpxData other = (GpxData) obj; 494 if (dataSources == null) { 495 if (other.dataSources != null) 496 return false; 497 } else if (!dataSources.equals(other.dataSources)) 498 return false; 499 if (routes == null) { 500 if (other.routes != null) 501 return false; 502 } else if (!routes.equals(other.routes)) 503 return false; 504 if (tracks == null) { 505 if (other.tracks != null) 506 return false; 507 } else if (!tracks.equals(other.tracks)) 508 return false; 509 if (waypoints == null) { 510 if (other.waypoints != null) 511 return false; 512 } else if (!waypoints.equals(other.waypoints)) 513 return false; 514 return true; 515 } 516}