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