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 Object obj = get(PT_TIME);
165                final Date time = obj instanceof Date ? (Date) obj : DateUtils.fromString(obj.toString());
166                this.time = time.getTime() / 1000.;
167                return time;
168            } catch (UncheckedParseException e) {
169                Logging.warn(e);
170                time = 0;
171            }
172        }
173        return null;
174    }
175
176    @Override
177    public int compareTo(WayPoint w) {
178        return Double.compare(time, w.time);
179    }
180
181    /**
182     * Returns the waypoint time.
183     * @return the waypoint time
184     */
185    public Date getTime() {
186        return new Date((long) (time * 1000));
187    }
188
189    @Override
190    public Object getTemplateValue(String name, boolean special) {
191        if (!special)
192            return get(name);
193        else
194            return null;
195    }
196
197    @Override
198    public boolean evaluateCondition(Match condition) {
199        throw new UnsupportedOperationException();
200    }
201
202    @Override
203    public List<String> getTemplateKeys() {
204        return new ArrayList<>(attr.keySet());
205    }
206
207    @Override
208    public int hashCode() {
209        final int prime = 31;
210        int result = super.hashCode();
211        long temp = Double.doubleToLongBits(lat);
212        result = prime * result + (int) (temp ^ (temp >>> 32));
213        temp = Double.doubleToLongBits(lon);
214        result = prime * result + (int) (temp ^ (temp >>> 32));
215        temp = Double.doubleToLongBits(time);
216        result = prime * result + (int) (temp ^ (temp >>> 32));
217        return result;
218    }
219
220    @Override
221    public boolean equals(Object obj) {
222        if (this == obj)
223            return true;
224        if (obj == null || !super.equals(obj) || getClass() != obj.getClass())
225            return false;
226        WayPoint other = (WayPoint) obj;
227        return Double.doubleToLongBits(lat) == Double.doubleToLongBits(other.lat)
228            && Double.doubleToLongBits(lon) == Double.doubleToLongBits(other.lon)
229            && Double.doubleToLongBits(time) == Double.doubleToLongBits(other.time);
230    }
231}