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