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 * Sets the {@link #PT_TIME} attribute to the specified time. 181 * 182 * @param ts seconds from the epoch 183 * @since 13210 184 */ 185 public void setTime(long ts) { 186 setTimeInMillis(ts * 1000); 187 } 188 189 /** 190 * Sets the {@link #PT_TIME} attribute to the specified time. 191 * 192 * @param ts milliseconds from the epoch 193 * @since 14434 194 */ 195 public void setTimeInMillis(long ts) { 196 attr.put(PT_TIME, new Date(ts)); 197 } 198 199 @Override 200 public int compareTo(WayPoint w) { 201 return Long.compare(getTimeInMillis(), w.getTimeInMillis()); 202 } 203 204 /** 205 * Returns the waypoint time in seconds since the epoch. 206 * 207 * @return the waypoint time 208 */ 209 public double getTime() { 210 return getTimeInMillis() / 1000.; 211 } 212 213 /** 214 * Returns the waypoint time in milliseconds since the epoch. 215 * 216 * @return the waypoint time 217 * @since 14456 218 */ 219 public long getTimeInMillis() { 220 Date d = getDateImpl(); 221 return d == null ? 0 : d.getTime(); 222 } 223 224 /** 225 * Returns true if this waypoint has a time. 226 * 227 * @return true if a time is set, false otherwise 228 * @since 14456 229 */ 230 public boolean hasDate() { 231 return attr.get(PT_TIME) instanceof Date; 232 } 233 234 /** 235 * Returns the waypoint time Date object. 236 * 237 * @return a copy of the Date object associated with this waypoint 238 * @since 14456 239 */ 240 public Date getDate() { 241 return DateUtils.cloneDate(getDateImpl()); 242 } 243 244 /** 245 * Returns the waypoint time Date object. 246 * 247 * @return the Date object associated with this waypoint 248 */ 249 private Date getDateImpl() { 250 if (attr != null) { 251 final Object obj = attr.get(PT_TIME); 252 253 if (obj instanceof Date) { 254 return (Date) obj; 255 } else if (obj == null) { 256 Logging.info("Waypoint {0} value unset", PT_TIME); 257 } else { 258 Logging.warn("Unsupported waypoint {0} value: {1}", PT_TIME, obj); 259 } 260 } 261 262 return null; 263 } 264 265 @Override 266 public Object getTemplateValue(String name, boolean special) { 267 if (!special) 268 return get(name); 269 else 270 return null; 271 } 272 273 @Override 274 public boolean evaluateCondition(Match condition) { 275 throw new UnsupportedOperationException(); 276 } 277 278 @Override 279 public List<String> getTemplateKeys() { 280 return new ArrayList<>(attr.keySet()); 281 } 282 283 @Override 284 public int hashCode() { 285 final int prime = 31; 286 int result = super.hashCode(); 287 long temp = Double.doubleToLongBits(lat); 288 result = prime * result + (int) (temp ^ (temp >>> 32)); 289 temp = Double.doubleToLongBits(lon); 290 result = prime * result + (int) (temp ^ (temp >>> 32)); 291 temp = getTimeInMillis(); 292 result = prime * result + (int) (temp ^ (temp >>> 32)); 293 return result; 294 } 295 296 @Override 297 public boolean equals(Object obj) { 298 if (this == obj) 299 return true; 300 if (obj == null || !super.equals(obj) || getClass() != obj.getClass()) 301 return false; 302 WayPoint other = (WayPoint) obj; 303 return Double.doubleToLongBits(lat) == Double.doubleToLongBits(other.lat) 304 && Double.doubleToLongBits(lon) == Double.doubleToLongBits(other.lon) 305 && getTimeInMillis() == other.getTimeInMillis(); 306 } 307}