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}