001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.gpx; 003 004import java.awt.Color; 005import java.util.ArrayList; 006import java.util.Date; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Objects; 010 011import org.openstreetmap.josm.data.coor.EastNorth; 012import org.openstreetmap.josm.data.coor.ILatLon; 013import org.openstreetmap.josm.data.coor.LatLon; 014import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 015import org.openstreetmap.josm.data.projection.Projecting; 016import org.openstreetmap.josm.tools.Logging; 017import org.openstreetmap.josm.tools.date.DateUtils; 018import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider; 019 020/** 021 * A point in the GPX data 022 * @since 12167 implements ILatLon 023 */ 024public class WayPoint extends WithAttributes implements Comparable<WayPoint>, TemplateEngineDataProvider, ILatLon { 025 026 /** 027 * The color to draw the segment before this point in 028 * @see #drawLine 029 */ 030 public Color customColoring; 031 032 /** 033 * <code>true</code> indicates that the line before this point should be drawn 034 */ 035 public boolean drawLine; 036 037 /** 038 * The direction of the line before this point. Used as cache to speed up drawing. Should not be relied on. 039 */ 040 public int dir; 041 042 /* 043 * We "inline" lat/lon, rather than using a LatLon internally => reduces memory overhead. Relevant 044 * because a lot of GPX waypoints are created when GPS tracks are downloaded from the OSM server. 045 */ 046 private final double lat; 047 private final double lon; 048 049 /* 050 * internal cache of projected coordinates 051 */ 052 private double east = Double.NaN; 053 private double north = Double.NaN; 054 private Object eastNorthCacheKey; 055 056 /** 057 * Constructs a new {@code WayPoint} from an existing one. 058 * 059 * Except for PT_TIME attribute, all attribute objects are shallow copied. 060 * This means modification of attr objects will affect original and new {@code WayPoint}. 061 * 062 * @param p existing waypoint 063 */ 064 public WayPoint(WayPoint p) { 065 attr = new LegacyMap(); 066 attr.putAll(p.attr); 067 attr.put(PT_TIME, p.getDate()); 068 lat = p.lat; 069 lon = p.lon; 070 east = p.east; 071 north = p.north; 072 eastNorthCacheKey = p.eastNorthCacheKey; 073 customColoring = p.customColoring; 074 drawLine = p.drawLine; 075 dir = p.dir; 076 } 077 078 /** 079 * Constructs a new {@code WayPoint} from lat/lon coordinates. 080 * @param ll lat/lon coordinates 081 */ 082 public WayPoint(LatLon ll) { 083 attr = new LegacyMap(); 084 lat = ll.lat(); 085 lon = ll.lon(); 086 } 087 088 /** 089 * Interim to detect legacy code that is not using {@code WayPoint.setTime(x)} 090 * functions, but {@code attr.put(PT_TIME, (String) x)} logic. 091 * To remove mid 2019 092 */ 093 private static class LegacyMap extends HashMap<String, Object> { 094 private static final long serialVersionUID = 1; 095 096 LegacyMap() { 097 super(0); 098 } 099 100 @Override 101 public Object put(String key, Object value) { 102 Object ret = null; 103 if (!PT_TIME.equals(key) || value instanceof Date) { 104 ret = super.put(key, value); 105 } else if (value instanceof String) { 106 ret = super.put(PT_TIME, DateUtils.fromString((String) value)); 107 List<String> lastErrorAndWarnings = Logging.getLastErrorAndWarnings(); 108 if (!lastErrorAndWarnings.isEmpty() && !lastErrorAndWarnings.get(0).contains("calling WayPoint.put")) { 109 StackTraceElement[] e = Thread.currentThread().getStackTrace(); 110 int n = 1; 111 while (n < e.length && "put".equals(e[n].getMethodName())) { 112 n++; 113 } 114 if (n < e.length) { 115 Logging.warn("{0}:{1} calling WayPoint.put(PT_TIME, ..) is deprecated. " + 116 "Use WayPoint.setTime(..) instead.", e[n].getClassName(), e[n].getMethodName()); 117 } 118 } 119 } 120 return ret; 121 } 122 } 123 124 /** 125 * Invalidate the internal cache of east/north coordinates. 126 */ 127 public void invalidateEastNorthCache() { 128 this.east = Double.NaN; 129 this.north = Double.NaN; 130 } 131 132 /** 133 * Returns the waypoint coordinates. 134 * @return the waypoint coordinates 135 */ 136 public final LatLon getCoor() { 137 return new LatLon(lat, lon); 138 } 139 140 @Override 141 public double lon() { 142 return lon; 143 } 144 145 @Override 146 public double lat() { 147 return lat; 148 } 149 150 @Override 151 public final EastNorth getEastNorth(Projecting projecting) { 152 Object newCacheKey = projecting.getCacheKey(); 153 if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(newCacheKey, this.eastNorthCacheKey)) { 154 // projected coordinates haven't been calculated yet, 155 // so fill the cache of the projected waypoint coordinates 156 EastNorth en = projecting.latlon2eastNorth(this); 157 this.east = en.east(); 158 this.north = en.north(); 159 this.eastNorthCacheKey = newCacheKey; 160 } 161 return new EastNorth(east, north); 162 } 163 164 @Override 165 public String toString() { 166 return "WayPoint (" + (attr.containsKey(GPX_NAME) ? get(GPX_NAME) + ", " : "") + getCoor() + ", " + attr + ')'; 167 } 168 169 /** 170 * Sets the {@link #PT_TIME} attribute to the specified time. 171 * 172 * @param time the time to set 173 * @since 9383 174 */ 175 public void setTime(Date time) { 176 setTimeInMillis(time.getTime()); 177 } 178 179 /** 180 * Convert the time stamp of the waypoint into seconds from the epoch. 181 * 182 * @deprecated Use {@link #setTime(Date)}, {@link #setTime(long)}, {@link #setTimeInMillis(long)} 183 */ 184 @Deprecated 185 public void setTime() { 186 setTimeFromAttribute(); 187 } 188 189 /** 190 * Sets the {@link #PT_TIME} attribute to the specified time. 191 * 192 * @param ts seconds from the epoch 193 * @since 13210 194 */ 195 public void setTime(long ts) { 196 setTimeInMillis(ts * 1000); 197 } 198 199 /** 200 * Sets the {@link #PT_TIME} attribute to the specified time. 201 * 202 * @param ts milliseconds from the epoch 203 * @since 14434 204 */ 205 public void setTimeInMillis(long ts) { 206 attr.put(PT_TIME, new Date(ts)); 207 } 208 209 /** 210 * Convert the time stamp of the waypoint into seconds from the epoch. 211 * @return The parsed time if successful, or {@code null} 212 * @since 9383 213 * @deprecated Use {@link #setTime(Date)}, {@link #setTime(long)}, {@link #setTimeInMillis(long)} 214 */ 215 @Deprecated 216 public Date setTimeFromAttribute() { 217 Logging.warn("WayPoint.setTimeFromAttribute() is deprecated, please fix calling code"); 218 return getDate(); 219 } 220 221 @Override 222 public int compareTo(WayPoint w) { 223 return Long.compare(getTimeInMillis(), w.getTimeInMillis()); 224 } 225 226 /** 227 * Returns the waypoint time in seconds since the epoch. 228 * 229 * @return the waypoint time 230 */ 231 public double getTime() { 232 return getTimeInMillis() / 1000.; 233 } 234 235 /** 236 * Returns the waypoint time in milliseconds since the epoch. 237 * 238 * @return the waypoint time 239 * @since 14456 240 */ 241 public long getTimeInMillis() { 242 Date d = getDateImpl(); 243 return d == null ? 0 : d.getTime(); 244 } 245 246 /** 247 * Returns true if this waypoint has a time. 248 * 249 * @return true if a time is set, false otherwise 250 * @since 14456 251 */ 252 public boolean hasDate() { 253 return attr.get(PT_TIME) instanceof Date; 254 } 255 256 /** 257 * Returns the waypoint time Date object. 258 * 259 * @return a copy of the Date object associated with this waypoint 260 * @since 14456 261 */ 262 public Date getDate() { 263 return DateUtils.cloneDate(getDateImpl()); 264 } 265 266 /** 267 * Returns the waypoint time Date object. 268 * 269 * @return the Date object associated with this waypoint 270 */ 271 private Date getDateImpl() { 272 if (attr != null) { 273 final Object obj = attr.get(PT_TIME); 274 275 if (obj instanceof Date) { 276 return (Date) obj; 277 } else if (obj == null) { 278 Logging.info("Waypoint {0} value unset", PT_TIME); 279 } else { 280 Logging.warn("Unsupported waypoint {0} value: {1}", PT_TIME, obj); 281 } 282 } 283 284 return null; 285 } 286 287 @Override 288 public Object getTemplateValue(String name, boolean special) { 289 if (!special) 290 return get(name); 291 else 292 return null; 293 } 294 295 @Override 296 public boolean evaluateCondition(Match condition) { 297 throw new UnsupportedOperationException(); 298 } 299 300 @Override 301 public List<String> getTemplateKeys() { 302 return new ArrayList<>(attr.keySet()); 303 } 304 305 @Override 306 public int hashCode() { 307 final int prime = 31; 308 int result = super.hashCode(); 309 long temp = Double.doubleToLongBits(lat); 310 result = prime * result + (int) (temp ^ (temp >>> 32)); 311 temp = Double.doubleToLongBits(lon); 312 result = prime * result + (int) (temp ^ (temp >>> 32)); 313 temp = getTimeInMillis(); 314 result = prime * result + (int) (temp ^ (temp >>> 32)); 315 return result; 316 } 317 318 @Override 319 public boolean equals(Object obj) { 320 if (this == obj) 321 return true; 322 if (obj == null || !super.equals(obj) || getClass() != obj.getClass()) 323 return false; 324 WayPoint other = (WayPoint) obj; 325 return Double.doubleToLongBits(lat) == Double.doubleToLongBits(other.lat) 326 && Double.doubleToLongBits(lon) == Double.doubleToLongBits(other.lon) 327 && getTimeInMillis() == other.getTimeInMillis(); 328 } 329}