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