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