001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.gpx;
003
004import java.awt.Color;
005import java.time.Instant;
006import java.util.ArrayList;
007import java.util.Date;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Objects;
011
012import org.openstreetmap.josm.data.coor.EastNorth;
013import org.openstreetmap.josm.data.coor.ILatLon;
014import org.openstreetmap.josm.data.coor.LatLon;
015import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
016import org.openstreetmap.josm.data.projection.Projecting;
017import org.openstreetmap.josm.tools.Logging;
018import org.openstreetmap.josm.tools.Utils;
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 HashMap<>(0);
067        attr.putAll(p.attr);
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 HashMap<>(0);
084        lat = ll.lat();
085        lon = ll.lon();
086    }
087
088    /**
089     * Invalidate the internal cache of east/north coordinates.
090     */
091    public void invalidateEastNorthCache() {
092        this.east = Double.NaN;
093        this.north = Double.NaN;
094    }
095
096    /**
097     * Returns the waypoint coordinates.
098     * @return the waypoint coordinates
099     */
100    public final LatLon getCoor() {
101        return new LatLon(lat, lon);
102    }
103
104    @Override
105    public double lon() {
106        return lon;
107    }
108
109    @Override
110    public double lat() {
111        return lat;
112    }
113
114    @Override
115    public final EastNorth getEastNorth(Projecting projecting) {
116        Object newCacheKey = projecting.getCacheKey();
117        if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(newCacheKey, this.eastNorthCacheKey)) {
118            // projected coordinates haven't been calculated yet,
119            // so fill the cache of the projected waypoint coordinates
120            EastNorth en = projecting.latlon2eastNorth(this);
121            this.east = en.east();
122            this.north = en.north();
123            this.eastNorthCacheKey = newCacheKey;
124        }
125        return new EastNorth(east, north);
126    }
127
128    @Override
129    public String toString() {
130        return "WayPoint (" + (attr.containsKey(GPX_NAME) ? get(GPX_NAME) + ", " : "") + getCoor() + ", " + attr + ')';
131    }
132
133    /**
134     * Sets the {@link #PT_TIME} attribute to the specified time.
135     *
136     * @param ts seconds from the epoch
137     * @since 13210
138     * @deprecated Use {@link #setInstant(Instant)}
139     */
140    @Deprecated
141    public void setTime(long ts) {
142        setInstant(Instant.ofEpochSecond(ts));
143    }
144
145    /**
146     * Sets the {@link #PT_TIME} attribute to the specified time.
147     *
148     * @param ts milliseconds from the epoch
149     * @since 14434
150     */
151    public void setTimeInMillis(long ts) {
152        setInstant(Instant.ofEpochMilli(ts));
153    }
154
155    /**
156     * Sets the {@link #PT_TIME} attribute to the specified time.
157     *
158     * @param instant the time to set
159     */
160    public void setInstant(Instant instant) {
161        attr.put(PT_TIME, instant);
162    }
163
164    @Override
165    public int compareTo(WayPoint w) {
166        return Long.compare(getTimeInMillis(), w.getTimeInMillis());
167    }
168
169    /**
170     * Returns the waypoint time in seconds since the epoch.
171     *
172     * @return the waypoint time
173     */
174    public double getTime() {
175        return getTimeInMillis() / 1000.;
176    }
177
178    /**
179     * Returns the waypoint time in milliseconds since the epoch.
180     *
181     * @return the waypoint time
182     * @since 14456
183     */
184    public long getTimeInMillis() {
185        Instant d = getInstant();
186        return d == null ? 0 : d.toEpochMilli();
187    }
188
189    /**
190     * Returns true if this waypoint has a time.
191     *
192     * @return true if a time is set, false otherwise
193     * @since 14456
194     */
195    public boolean hasDate() {
196        return attr.get(PT_TIME) instanceof Instant;
197    }
198
199    /**
200     * Returns the waypoint time Date object.
201     *
202     * @return a copy of the Date object associated with this waypoint
203     * @since 14456
204     * @deprecated Use {@link #getInstant()}
205     */
206    @Deprecated
207    public Date getDate() {
208        Instant instant = getInstant();
209        return instant == null ? null : Date.from(instant);
210    }
211
212    /**
213     * Returns the waypoint instant.
214     *
215     * @return the instant associated with this waypoint
216     * @since 14456
217     */
218    public Instant getInstant() {
219        if (attr != null) {
220            final Object obj = attr.get(PT_TIME);
221
222            if (obj instanceof Instant) {
223                return (Instant) obj;
224            } else if (obj == null) {
225                Logging.trace("Waypoint {0} value unset", PT_TIME);
226            } else {
227                Logging.warn("Unsupported waypoint {0} value: {1}", PT_TIME, obj);
228            }
229        }
230
231        return null;
232    }
233
234    @Override
235    public Object getTemplateValue(String name, boolean special) {
236        if (special) {
237            return null;
238        } else if ("desc".equals(name)) {
239            final Object value = get(name);
240            return value instanceof String ? Utils.stripHtml(((String) value)) : value;
241        } else {
242            return get(name);
243        }
244    }
245
246    @Override
247    public boolean evaluateCondition(Match condition) {
248        throw new UnsupportedOperationException();
249    }
250
251    @Override
252    public List<String> getTemplateKeys() {
253        return new ArrayList<>(attr.keySet());
254    }
255
256    @Override
257    public int hashCode() {
258        return Objects.hash(super.hashCode(), lat, lon, getTimeInMillis());
259    }
260
261    @Override
262    public boolean equals(Object obj) {
263        if (this == obj)
264            return true;
265        if (obj == null || !super.equals(obj) || getClass() != obj.getClass())
266            return false;
267        WayPoint other = (WayPoint) obj;
268        return Double.doubleToLongBits(lat) == Double.doubleToLongBits(other.lat)
269            && Double.doubleToLongBits(lon) == Double.doubleToLongBits(other.lon)
270            && getTimeInMillis() == other.getTimeInMillis();
271    }
272}