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}