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