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