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.List;
008import java.util.Objects;
009
010import org.openstreetmap.josm.data.coor.EastNorth;
011import org.openstreetmap.josm.data.coor.ILatLon;
012import org.openstreetmap.josm.data.coor.LatLon;
013import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
014import org.openstreetmap.josm.data.projection.Projecting;
015import org.openstreetmap.josm.tools.Logging;
016import org.openstreetmap.josm.tools.UncheckedParseException;
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 seconds (not milliseconds!) since 1970-01-01 00:00 UTC
028     */
029    public double time;
030    /**
031     * The color to draw the segment before this point in
032     * @see #drawLine
033     */
034    public Color customColoring;
035    /**
036     * <code>true</code> indicates that the line before this point should be drawn
037     */
038    public boolean drawLine;
039    /**
040     * The direction of the line before this point. Used as cache to speed up drawing. Should not be relied on.
041     */
042    public int dir;
043
044    /**
045     * Constructs a new {@code WayPoint} from an existing one.
046     * @param p existing waypoint
047     */
048    public WayPoint(WayPoint p) {
049        attr.putAll(p.attr);
050        lat = p.lat;
051        lon = p.lon;
052        east = p.east;
053        north = p.north;
054        eastNorthCacheKey = p.eastNorthCacheKey;
055        time = p.time;
056        customColoring = p.customColoring;
057        drawLine = p.drawLine;
058        dir = p.dir;
059    }
060
061    /**
062     * Constructs a new {@code WayPoint} from lat/lon coordinates.
063     * @param ll lat/lon coordinates
064     */
065    public WayPoint(LatLon ll) {
066        lat = ll.lat();
067        lon = ll.lon();
068    }
069
070    /*
071     * We "inline" lat/lon, rather than usinga LatLon internally => reduces memory overhead. Relevant
072     * because a lot of GPX waypoints are created when GPS tracks are downloaded from the OSM server.
073     */
074    private final double lat;
075    private final double lon;
076
077    /*
078     * internal cache of projected coordinates
079     */
080    private double east = Double.NaN;
081    private double north = Double.NaN;
082    private Object eastNorthCacheKey;
083
084    /**
085     * Invalidate the internal cache of east/north coordinates.
086     */
087    public void invalidateEastNorthCache() {
088        this.east = Double.NaN;
089        this.north = Double.NaN;
090    }
091
092    /**
093     * Returns the waypoint coordinates.
094     * @return the waypoint coordinates
095     */
096    public final LatLon getCoor() {
097        return new LatLon(lat, lon);
098    }
099
100    @Override
101    public double lon() {
102        return lon;
103    }
104
105    @Override
106    public double lat() {
107        return lat;
108    }
109
110    @Override
111    public final EastNorth getEastNorth(Projecting projecting) {
112        Object newCacheKey = projecting.getCacheKey();
113        if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(newCacheKey, this.eastNorthCacheKey)) {
114            // projected coordinates haven't been calculated yet,
115            // so fill the cache of the projected waypoint coordinates
116            EastNorth en = projecting.latlon2eastNorth(this);
117            this.east = en.east();
118            this.north = en.north();
119            this.eastNorthCacheKey = newCacheKey;
120        }
121        return new EastNorth(east, north);
122    }
123
124    @Override
125    public String toString() {
126        return "WayPoint (" + (attr.containsKey(GPX_NAME) ? get(GPX_NAME) + ", " : "") + getCoor() + ", " + attr + ')';
127    }
128
129    /**
130     * Sets the {@link #time} field as well as the {@link #PT_TIME} attribute to the specified time
131     *
132     * @param time the time to set
133     * @since 9383
134     */
135    public void setTime(Date time) {
136        this.time = time.getTime() / 1000.;
137        this.attr.put(PT_TIME, DateUtils.fromDate(time));
138    }
139
140    /**
141     * Convert the time stamp of the waypoint into seconds from the epoch
142     */
143    public void setTime() {
144        setTimeFromAttribute();
145    }
146
147    /**
148     * Set the the time stamp of the waypoint into seconds from the epoch,
149     * @param time millisecond from the epoch
150     * @since 13210
151     */
152    public void setTime(long time) {
153        this.time = time / 1000.;
154    }
155
156    /**
157     * Convert the time stamp of the waypoint into seconds from the epoch
158     * @return The parsed time if successful, or {@code null}
159     * @since 9383
160     */
161    public Date setTimeFromAttribute() {
162        if (attr.containsKey(PT_TIME)) {
163            try {
164                final Date time = DateUtils.fromString(get(PT_TIME).toString());
165                this.time = time.getTime() / 1000.;
166                return time;
167            } catch (UncheckedParseException e) {
168                Logging.warn(e);
169                time = 0;
170            }
171        }
172        return null;
173    }
174
175    @Override
176    public int compareTo(WayPoint w) {
177        return Double.compare(time, w.time);
178    }
179
180    /**
181     * Returns the waypoint time.
182     * @return the waypoint time
183     */
184    public Date getTime() {
185        return new Date((long) (time * 1000));
186    }
187
188    @Override
189    public Object getTemplateValue(String name, boolean special) {
190        if (!special)
191            return get(name);
192        else
193            return null;
194    }
195
196    @Override
197    public boolean evaluateCondition(Match condition) {
198        throw new UnsupportedOperationException();
199    }
200
201    @Override
202    public List<String> getTemplateKeys() {
203        return new ArrayList<>(attr.keySet());
204    }
205
206    @Override
207    public int hashCode() {
208        final int prime = 31;
209        int result = super.hashCode();
210        long temp = Double.doubleToLongBits(lat);
211        result = prime * result + (int) (temp ^ (temp >>> 32));
212        temp = Double.doubleToLongBits(lon);
213        result = prime * result + (int) (temp ^ (temp >>> 32));
214        temp = Double.doubleToLongBits(time);
215        result = prime * result + (int) (temp ^ (temp >>> 32));
216        return result;
217    }
218
219    @Override
220    public boolean equals(Object obj) {
221        if (this == obj)
222            return true;
223        if (obj == null || !super.equals(obj) || getClass() != obj.getClass())
224            return false;
225        WayPoint other = (WayPoint) obj;
226        return Double.doubleToLongBits(lat) == Double.doubleToLongBits(other.lat)
227            && Double.doubleToLongBits(lon) == Double.doubleToLongBits(other.lon)
228            && Double.doubleToLongBits(time) == Double.doubleToLongBits(other.time);
229    }
230}