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}