001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.awt.geom.Area;
005import java.util.Collection;
006import java.util.Objects;
007import java.util.Set;
008import java.util.TreeSet;
009import java.util.function.Predicate;
010
011import org.openstreetmap.josm.Main;
012import org.openstreetmap.josm.data.coor.EastNorth;
013import org.openstreetmap.josm.data.coor.LatLon;
014import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
015import org.openstreetmap.josm.data.osm.visitor.Visitor;
016import org.openstreetmap.josm.data.projection.Projection;
017import org.openstreetmap.josm.data.projection.Projections;
018import org.openstreetmap.josm.tools.CheckParameterUtil;
019import org.openstreetmap.josm.tools.Utils;
020
021/**
022 * One node data, consisting of one world coordinate waypoint.
023 *
024 * @author imi
025 */
026public final class Node extends OsmPrimitive implements INode {
027
028    /*
029     * We "inline" lat/lon rather than using a LatLon-object => reduces memory footprint
030     */
031    private double lat = Double.NaN;
032    private double lon = Double.NaN;
033
034    /*
035     * the cached projected coordinates
036     */
037    private double east = Double.NaN;
038    private double north = Double.NaN;
039    /**
040     * The cache key to use for {@link #east} and {@link #north}.
041     */
042    private Object eastNorthCacheKey;
043
044    /**
045     * Determines if this node has valid coordinates.
046     * @return {@code true} if this node has valid coordinates
047     * @since 7828
048     */
049    public boolean isLatLonKnown() {
050        return !Double.isNaN(lat) && !Double.isNaN(lon);
051    }
052
053    @Override
054    public void setCoor(LatLon coor) {
055        updateCoor(coor, null);
056    }
057
058    @Override
059    public void setEastNorth(EastNorth eastNorth) {
060        updateCoor(null, eastNorth);
061    }
062
063    private void updateCoor(LatLon coor, EastNorth eastNorth) {
064        if (getDataSet() != null) {
065            boolean locked = writeLock();
066            try {
067                getDataSet().fireNodeMoved(this, coor, eastNorth);
068            } finally {
069                writeUnlock(locked);
070            }
071        } else {
072            setCoorInternal(coor, eastNorth);
073        }
074    }
075
076    /**
077     * Returns lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
078     * @return lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
079     */
080    @Override
081    public LatLon getCoor() {
082        if (!isLatLonKnown()) return null;
083        return new LatLon(lat, lon);
084    }
085
086    /**
087     * <p>Replies the projected east/north coordinates.</p>
088     *
089     * <p>Uses the {@link Main#getProjection() global projection} to project the lan/lon-coordinates.
090     * Internally caches the projected coordinates.</p>
091     *
092     * <p>Replies {@code null} if this node doesn't know lat/lon-coordinates, i.e. because it is an incomplete node.
093     *
094     * @return the east north coordinates or {@code null}
095     * @see #invalidateEastNorthCache()
096     *
097     */
098    @Override
099    public EastNorth getEastNorth() {
100        return getEastNorth(Main.getProjection());
101    }
102
103    /**
104     * Replies the projected east/north coordinates.
105     * <p>
106     * The result of the last conversion is cached. The cache object is used as cache key.
107     * @param projection The projection to use.
108     * @return The projected east/north coordinates
109     * @since 10827
110     */
111    public EastNorth getEastNorth(Projection projection) {
112        if (!isLatLonKnown()) return null;
113
114        if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(projection.getCacheKey(), eastNorthCacheKey)) {
115            // projected coordinates haven't been calculated yet,
116            // so fill the cache of the projected node coordinates
117            EastNorth en = Projections.project(new LatLon(lat, lon));
118            this.east = en.east();
119            this.north = en.north();
120            this.eastNorthCacheKey = projection.getCacheKey();
121        }
122        return new EastNorth(east, north);
123    }
124
125    /**
126     * To be used only by Dataset.reindexNode
127     * @param coor lat/lon
128     * @param eastNorth east/north
129     */
130    void setCoorInternal(LatLon coor, EastNorth eastNorth) {
131        if (coor != null) {
132            this.lat = coor.lat();
133            this.lon = coor.lon();
134            invalidateEastNorthCache();
135        } else if (eastNorth != null) {
136            LatLon ll = Projections.inverseProject(eastNorth);
137            this.lat = ll.lat();
138            this.lon = ll.lon();
139            this.east = eastNorth.east();
140            this.north = eastNorth.north();
141            this.eastNorthCacheKey = Main.getProjection().getCacheKey();
142        } else {
143            this.lat = Double.NaN;
144            this.lon = Double.NaN;
145            invalidateEastNorthCache();
146            if (isVisible()) {
147                setIncomplete(true);
148            }
149        }
150    }
151
152    protected Node(long id, boolean allowNegative) {
153        super(id, allowNegative);
154    }
155
156    /**
157     * Constructs a new local {@code Node} with id 0.
158     */
159    public Node() {
160        this(0, false);
161    }
162
163    /**
164     * Constructs an incomplete {@code Node} object with the given id.
165     * @param id The id. Must be &gt;= 0
166     * @throws IllegalArgumentException if id &lt; 0
167     */
168    public Node(long id) {
169        super(id, false);
170    }
171
172    /**
173     * Constructs a new {@code Node} with the given id and version.
174     * @param id The id. Must be &gt;= 0
175     * @param version The version
176     * @throws IllegalArgumentException if id &lt; 0
177     */
178    public Node(long id, int version) {
179        super(id, version, false);
180    }
181
182    /**
183     * Constructs an identical clone of the argument.
184     * @param clone The node to clone
185     * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
186     * If {@code false}, does nothing
187     */
188    public Node(Node clone, boolean clearMetadata) {
189        super(clone.getUniqueId(), true /* allow negative IDs */);
190        cloneFrom(clone);
191        if (clearMetadata) {
192            clearOsmMetadata();
193        }
194    }
195
196    /**
197     * Constructs an identical clone of the argument (including the id).
198     * @param clone The node to clone, including its id
199     */
200    public Node(Node clone) {
201        this(clone, false);
202    }
203
204    /**
205     * Constructs a new {@code Node} with the given lat/lon with id 0.
206     * @param latlon The {@link LatLon} coordinates
207     */
208    public Node(LatLon latlon) {
209        super(0, false);
210        setCoor(latlon);
211    }
212
213    /**
214     * Constructs a new {@code Node} with the given east/north with id 0.
215     * @param eastNorth The {@link EastNorth} coordinates
216     */
217    public Node(EastNorth eastNorth) {
218        super(0, false);
219        setEastNorth(eastNorth);
220    }
221
222    @Override
223    void setDataset(DataSet dataSet) {
224        super.setDataset(dataSet);
225        if (!isIncomplete() && isVisible() && !isLatLonKnown())
226            throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString());
227    }
228
229    @Override
230    public void accept(Visitor visitor) {
231        visitor.visit(this);
232    }
233
234    @Override
235    public void accept(PrimitiveVisitor visitor) {
236        visitor.visit(this);
237    }
238
239    @Override
240    public void cloneFrom(OsmPrimitive osm) {
241        if (!(osm instanceof Node))
242            throw new IllegalArgumentException("Not a node: " + osm);
243        boolean locked = writeLock();
244        try {
245            super.cloneFrom(osm);
246            setCoor(((Node) osm).getCoor());
247        } finally {
248            writeUnlock(locked);
249        }
250    }
251
252    /**
253     * Merges the technical and semantical attributes from <code>other</code> onto this.
254     *
255     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
256     * have an assigend OSM id, the IDs have to be the same.
257     *
258     * @param other the other primitive. Must not be null.
259     * @throws IllegalArgumentException if other is null.
260     * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
261     * @throws DataIntegrityProblemException if other is new and other.getId() != this.getId()
262     */
263    @Override
264    public void mergeFrom(OsmPrimitive other) {
265        if (!(other instanceof Node))
266            throw new IllegalArgumentException("Not a node: " + other);
267        boolean locked = writeLock();
268        try {
269            super.mergeFrom(other);
270            if (!other.isIncomplete()) {
271                setCoor(((Node) other).getCoor());
272            }
273        } finally {
274            writeUnlock(locked);
275        }
276    }
277
278    @Override
279    public void load(PrimitiveData data) {
280        if (!(data instanceof NodeData))
281            throw new IllegalArgumentException("Not a node data: " + data);
282        boolean locked = writeLock();
283        try {
284            super.load(data);
285            setCoor(((NodeData) data).getCoor());
286        } finally {
287            writeUnlock(locked);
288        }
289    }
290
291    @Override
292    public NodeData save() {
293        NodeData data = new NodeData();
294        saveCommonAttributes(data);
295        if (!isIncomplete()) {
296            data.setCoor(getCoor());
297        }
298        return data;
299    }
300
301    @Override
302    public String toString() {
303        String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : "";
304        return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}';
305    }
306
307    @Override
308    public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
309        return (other instanceof Node)
310                && hasEqualSemanticFlags(other)
311                && hasEqualCoordinates((Node) other)
312                && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
313    }
314
315    private boolean hasEqualCoordinates(Node other) {
316        final LatLon c1 = getCoor();
317        final LatLon c2 = other.getCoor();
318        return (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.equalsEpsilon(c2));
319    }
320
321    @Override
322    public int compareTo(OsmPrimitive o) {
323        return o instanceof Node ? Long.compare(getUniqueId(), o.getUniqueId()) : 1;
324    }
325
326    @Override
327    public String getDisplayName(NameFormatter formatter) {
328        return formatter.format(this);
329    }
330
331    @Override
332    public OsmPrimitiveType getType() {
333        return OsmPrimitiveType.NODE;
334    }
335
336    @Override
337    public BBox getBBox() {
338        return new BBox(lon, lat);
339    }
340
341    @Override
342    protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
343        box.add(lon, lat);
344    }
345
346    @Override
347    public void updatePosition() {
348        // Do nothing
349    }
350
351    @Override
352    public boolean isDrawable() {
353        // Not possible to draw a node without coordinates.
354        return super.isDrawable() && isLatLonKnown();
355    }
356
357    /**
358     * Check whether this node connects 2 ways.
359     *
360     * @return true if isReferredByWays(2) returns true
361     * @see #isReferredByWays(int)
362     */
363    public boolean isConnectionNode() {
364        return isReferredByWays(2);
365    }
366
367    /**
368     * Invoke to invalidate the internal cache of projected east/north coordinates.
369     * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked
370     * next time.
371     */
372    public void invalidateEastNorthCache() {
373        this.east = Double.NaN;
374        this.north = Double.NaN;
375        this.eastNorthCacheKey = null;
376    }
377
378    @Override
379    public boolean concernsArea() {
380        // A node cannot be an area
381        return false;
382    }
383
384    /**
385     * Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes
386     * matching the {@code predicate} (which may be {@code null} to consider all nodes).
387     * @param otherNodes other nodes
388     * @param hops number of hops
389     * @param predicate predicate to match
390     * @return {@code true} if {@code this} node mets the conditions
391     */
392    public boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate) {
393        CheckParameterUtil.ensureParameterNotNull(otherNodes);
394        CheckParameterUtil.ensureThat(!otherNodes.isEmpty(), "otherNodes must not be empty!");
395        CheckParameterUtil.ensureThat(hops >= 0, "hops must be non-negative!");
396        return hops == 0
397                ? isConnectedTo(otherNodes, hops, predicate, null)
398                : isConnectedTo(otherNodes, hops, predicate, new TreeSet<Node>());
399    }
400
401    private boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate, Set<Node> visited) {
402        if (otherNodes.contains(this)) {
403            return true;
404        }
405        if (hops > 0) {
406            visited.add(this);
407            for (final Way w : Utils.filteredCollection(this.getReferrers(), Way.class)) {
408                for (final Node n : w.getNodes()) {
409                    final boolean containsN = visited.contains(n);
410                    visited.add(n);
411                    if (!containsN && (predicate == null || predicate.test(n))
412                            && n.isConnectedTo(otherNodes, hops - 1, predicate, visited)) {
413                        return true;
414                    }
415                }
416            }
417        }
418        return false;
419    }
420
421    @Override
422    public boolean isOutsideDownloadArea() {
423        if (isNewOrUndeleted() || getDataSet() == null)
424            return false;
425        Area area = getDataSet().getDataSourceArea();
426        if (area == null)
427            return false;
428        LatLon coor = getCoor();
429        return coor != null && !coor.isIn(area);
430    }
431}