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;
011
012import org.openstreetmap.josm.Main;
013import org.openstreetmap.josm.data.coor.EastNorth;
014import org.openstreetmap.josm.data.coor.LatLon;
015import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
016import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
017import org.openstreetmap.josm.data.projection.Projecting;
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    @Override
045    public void setCoor(LatLon coor) {
046        updateCoor(coor, null);
047    }
048
049    @Override
050    public void setEastNorth(EastNorth eastNorth) {
051        updateCoor(null, eastNorth);
052    }
053
054    private void updateCoor(LatLon coor, EastNorth eastNorth) {
055        if (getDataSet() != null) {
056            boolean locked = writeLock();
057            try {
058                getDataSet().fireNodeMoved(this, coor, eastNorth);
059            } finally {
060                writeUnlock(locked);
061            }
062        } else {
063            setCoorInternal(coor, eastNorth);
064        }
065    }
066
067    /**
068     * Returns lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
069     * @return lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
070     */
071    @Override
072    public LatLon getCoor() {
073        if (!isLatLonKnown()) {
074            return null;
075        } else {
076            return new LatLon(lat, lon);
077        }
078    }
079
080    @Override
081    public double lat() {
082        return lat;
083    }
084
085    @Override
086    public double lon() {
087        return lon;
088    }
089
090    @Override
091    public EastNorth getEastNorth(Projecting projection) {
092        if (!isLatLonKnown()) return null;
093
094        if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(projection.getCacheKey(), eastNorthCacheKey)) {
095            // projected coordinates haven't been calculated yet,
096            // so fill the cache of the projected node coordinates
097            EastNorth en = projection.latlon2eastNorth(this);
098            this.east = en.east();
099            this.north = en.north();
100            this.eastNorthCacheKey = projection.getCacheKey();
101        }
102        return new EastNorth(east, north);
103    }
104
105    /**
106     * To be used only by Dataset.reindexNode
107     * @param coor lat/lon
108     * @param eastNorth east/north
109     */
110    void setCoorInternal(LatLon coor, EastNorth eastNorth) {
111        if (coor != null) {
112            this.lat = coor.lat();
113            this.lon = coor.lon();
114            invalidateEastNorthCache();
115        } else if (eastNorth != null) {
116            LatLon ll = Main.getProjection().eastNorth2latlon(eastNorth);
117            this.lat = ll.lat();
118            this.lon = ll.lon();
119            this.east = eastNorth.east();
120            this.north = eastNorth.north();
121            this.eastNorthCacheKey = Main.getProjection().getCacheKey();
122        } else {
123            this.lat = Double.NaN;
124            this.lon = Double.NaN;
125            invalidateEastNorthCache();
126            if (isVisible()) {
127                setIncomplete(true);
128            }
129        }
130    }
131
132    protected Node(long id, boolean allowNegative) {
133        super(id, allowNegative);
134    }
135
136    /**
137     * Constructs a new local {@code Node} with id 0.
138     */
139    public Node() {
140        this(0, false);
141    }
142
143    /**
144     * Constructs an incomplete {@code Node} object with the given id.
145     * @param id The id. Must be >= 0
146     * @throws IllegalArgumentException if id < 0
147     */
148    public Node(long id) {
149        super(id, false);
150    }
151
152    /**
153     * Constructs a new {@code Node} with the given id and version.
154     * @param id The id. Must be >= 0
155     * @param version The version
156     * @throws IllegalArgumentException if id < 0
157     */
158    public Node(long id, int version) {
159        super(id, version, false);
160    }
161
162    /**
163     * Constructs an identical clone of the argument.
164     * @param clone The node to clone
165     * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
166     * If {@code false}, does nothing
167     */
168    public Node(Node clone, boolean clearMetadata) {
169        super(clone.getUniqueId(), true /* allow negative IDs */);
170        cloneFrom(clone);
171        if (clearMetadata) {
172            clearOsmMetadata();
173        }
174    }
175
176    /**
177     * Constructs an identical clone of the argument (including the id).
178     * @param clone The node to clone, including its id
179     */
180    public Node(Node clone) {
181        this(clone, false);
182    }
183
184    /**
185     * Constructs a new {@code Node} with the given lat/lon with id 0.
186     * @param latlon The {@link LatLon} coordinates
187     */
188    public Node(LatLon latlon) {
189        super(0, false);
190        setCoor(latlon);
191    }
192
193    /**
194     * Constructs a new {@code Node} with the given east/north with id 0.
195     * @param eastNorth The {@link EastNorth} coordinates
196     */
197    public Node(EastNorth eastNorth) {
198        super(0, false);
199        setEastNorth(eastNorth);
200    }
201
202    @Override
203    void setDataset(DataSet dataSet) {
204        super.setDataset(dataSet);
205        if (!isIncomplete() && isVisible() && !isLatLonKnown())
206            throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString());
207    }
208
209    @Override
210    public void accept(OsmPrimitiveVisitor visitor) {
211        visitor.visit(this);
212    }
213
214    @Override
215    public void accept(PrimitiveVisitor visitor) {
216        visitor.visit(this);
217    }
218
219    @Override
220    public void cloneFrom(OsmPrimitive osm) {
221        if (!(osm instanceof Node))
222            throw new IllegalArgumentException("Not a node: " + osm);
223        boolean locked = writeLock();
224        try {
225            super.cloneFrom(osm);
226            setCoor(((Node) osm).getCoor());
227        } finally {
228            writeUnlock(locked);
229        }
230    }
231
232    /**
233     * Merges the technical and semantical attributes from <code>other</code> onto this.
234     *
235     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
236     * have an assigend OSM id, the IDs have to be the same.
237     *
238     * @param other the other primitive. Must not be null.
239     * @throws IllegalArgumentException if other is null.
240     * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
241     * @throws DataIntegrityProblemException if other is new and other.getId() != this.getId()
242     */
243    @Override
244    public void mergeFrom(OsmPrimitive other) {
245        if (!(other instanceof Node))
246            throw new IllegalArgumentException("Not a node: " + other);
247        boolean locked = writeLock();
248        try {
249            super.mergeFrom(other);
250            if (!other.isIncomplete()) {
251                setCoor(((Node) other).getCoor());
252            }
253        } finally {
254            writeUnlock(locked);
255        }
256    }
257
258    @Override
259    public void load(PrimitiveData data) {
260        if (!(data instanceof NodeData))
261            throw new IllegalArgumentException("Not a node data: " + data);
262        boolean locked = writeLock();
263        try {
264            super.load(data);
265            setCoor(((NodeData) data).getCoor());
266        } finally {
267            writeUnlock(locked);
268        }
269    }
270
271    @Override
272    public NodeData save() {
273        NodeData data = new NodeData();
274        saveCommonAttributes(data);
275        if (!isIncomplete()) {
276            data.setCoor(getCoor());
277        }
278        return data;
279    }
280
281    @Override
282    public String toString() {
283        String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : "";
284        return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}';
285    }
286
287    @Override
288    public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
289        return (other instanceof Node)
290                && hasEqualSemanticFlags(other)
291                && hasEqualCoordinates((Node) other)
292                && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
293    }
294
295    private boolean hasEqualCoordinates(Node other) {
296        final LatLon c1 = getCoor();
297        final LatLon c2 = other.getCoor();
298        return (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.equalsEpsilon(c2));
299    }
300
301    @Override
302    public OsmPrimitiveType getType() {
303        return OsmPrimitiveType.NODE;
304    }
305
306    @Override
307    public BBox getBBox() {
308        return new BBox(lon, lat);
309    }
310
311    @Override
312    protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
313        box.add(lon, lat);
314    }
315
316    @Override
317    public void updatePosition() {
318        // Do nothing
319    }
320
321    @Override
322    public boolean isDrawable() {
323        // Not possible to draw a node without coordinates.
324        return super.isDrawable() && isLatLonKnown();
325    }
326
327    @Override
328    public boolean isReferredByWays(int n) {
329        return isNodeReferredByWays(n);
330    }
331
332    /**
333     * Invoke to invalidate the internal cache of projected east/north coordinates.
334     * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked
335     * next time.
336     */
337    public void invalidateEastNorthCache() {
338        this.east = Double.NaN;
339        this.north = Double.NaN;
340        this.eastNorthCacheKey = null;
341    }
342
343    @Override
344    public boolean concernsArea() {
345        // A node cannot be an area
346        return false;
347    }
348
349    /**
350     * Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes
351     * matching the {@code predicate} (which may be {@code null} to consider all nodes).
352     * @param otherNodes other nodes
353     * @param hops number of hops
354     * @param predicate predicate to match
355     * @return {@code true} if {@code this} node mets the conditions
356     */
357    public boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate) {
358        CheckParameterUtil.ensureParameterNotNull(otherNodes);
359        CheckParameterUtil.ensureThat(!otherNodes.isEmpty(), "otherNodes must not be empty!");
360        CheckParameterUtil.ensureThat(hops >= 0, "hops must be non-negative!");
361        return hops == 0
362                ? isConnectedTo(otherNodes, hops, predicate, null)
363                : isConnectedTo(otherNodes, hops, predicate, new TreeSet<>());
364    }
365
366    private boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate, Set<Node> visited) {
367        if (otherNodes.contains(this)) {
368            return true;
369        }
370        if (hops > 0 && visited != null) {
371            visited.add(this);
372            for (final Way w : Utils.filteredCollection(this.getReferrers(), Way.class)) {
373                for (final Node n : w.getNodes()) {
374                    final boolean containsN = visited.contains(n);
375                    visited.add(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 getFilteredList(getReferrers(), Way.class);
404    }
405}