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    /**
091     * Replies the projected east/north coordinates.
092     * <p>
093     * Uses the {@link Main#getProjection() global projection} to project the lat/lon-coordinates.
094     * <p>
095     * @return the east north coordinates or {@code null} if {@link #isLatLonKnown()}
096     * is false.
097     */
098    public EastNorth getEastNorth() {
099        return getEastNorth(Main.getProjection());
100    }
101
102    @Override
103    public EastNorth getEastNorth(Projecting projection) {
104        if (!isLatLonKnown()) return null;
105
106        if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(projection.getCacheKey(), eastNorthCacheKey)) {
107            // projected coordinates haven't been calculated yet,
108            // so fill the cache of the projected node coordinates
109            EastNorth en = projection.latlon2eastNorth(this);
110            this.east = en.east();
111            this.north = en.north();
112            this.eastNorthCacheKey = projection.getCacheKey();
113        }
114        return new EastNorth(east, north);
115    }
116
117    /**
118     * To be used only by Dataset.reindexNode
119     * @param coor lat/lon
120     * @param eastNorth east/north
121     */
122    void setCoorInternal(LatLon coor, EastNorth eastNorth) {
123        if (coor != null) {
124            this.lat = coor.lat();
125            this.lon = coor.lon();
126            invalidateEastNorthCache();
127        } else if (eastNorth != null) {
128            LatLon ll = Main.getProjection().eastNorth2latlon(eastNorth);
129            this.lat = ll.lat();
130            this.lon = ll.lon();
131            this.east = eastNorth.east();
132            this.north = eastNorth.north();
133            this.eastNorthCacheKey = Main.getProjection().getCacheKey();
134        } else {
135            this.lat = Double.NaN;
136            this.lon = Double.NaN;
137            invalidateEastNorthCache();
138            if (isVisible()) {
139                setIncomplete(true);
140            }
141        }
142    }
143
144    protected Node(long id, boolean allowNegative) {
145        super(id, allowNegative);
146    }
147
148    /**
149     * Constructs a new local {@code Node} with id 0.
150     */
151    public Node() {
152        this(0, false);
153    }
154
155    /**
156     * Constructs an incomplete {@code Node} object with the given id.
157     * @param id The id. Must be &gt;= 0
158     * @throws IllegalArgumentException if id &lt; 0
159     */
160    public Node(long id) {
161        super(id, false);
162    }
163
164    /**
165     * Constructs a new {@code Node} with the given id and version.
166     * @param id The id. Must be &gt;= 0
167     * @param version The version
168     * @throws IllegalArgumentException if id &lt; 0
169     */
170    public Node(long id, int version) {
171        super(id, version, false);
172    }
173
174    /**
175     * Constructs an identical clone of the argument.
176     * @param clone The node to clone
177     * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
178     * If {@code false}, does nothing
179     */
180    public Node(Node clone, boolean clearMetadata) {
181        super(clone.getUniqueId(), true /* allow negative IDs */);
182        cloneFrom(clone);
183        if (clearMetadata) {
184            clearOsmMetadata();
185        }
186    }
187
188    /**
189     * Constructs an identical clone of the argument (including the id).
190     * @param clone The node to clone, including its id
191     */
192    public Node(Node clone) {
193        this(clone, false);
194    }
195
196    /**
197     * Constructs a new {@code Node} with the given lat/lon with id 0.
198     * @param latlon The {@link LatLon} coordinates
199     */
200    public Node(LatLon latlon) {
201        super(0, false);
202        setCoor(latlon);
203    }
204
205    /**
206     * Constructs a new {@code Node} with the given east/north with id 0.
207     * @param eastNorth The {@link EastNorth} coordinates
208     */
209    public Node(EastNorth eastNorth) {
210        super(0, false);
211        setEastNorth(eastNorth);
212    }
213
214    @Override
215    void setDataset(DataSet dataSet) {
216        super.setDataset(dataSet);
217        if (!isIncomplete() && isVisible() && !isLatLonKnown())
218            throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString());
219    }
220
221    @Override
222    public void accept(OsmPrimitiveVisitor visitor) {
223        visitor.visit(this);
224    }
225
226    @Override
227    public void accept(PrimitiveVisitor visitor) {
228        visitor.visit(this);
229    }
230
231    @Override
232    public void cloneFrom(OsmPrimitive osm) {
233        if (!(osm instanceof Node))
234            throw new IllegalArgumentException("Not a node: " + osm);
235        boolean locked = writeLock();
236        try {
237            super.cloneFrom(osm);
238            setCoor(((Node) osm).getCoor());
239        } finally {
240            writeUnlock(locked);
241        }
242    }
243
244    /**
245     * Merges the technical and semantical attributes from <code>other</code> onto this.
246     *
247     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
248     * have an assigend OSM id, the IDs have to be the same.
249     *
250     * @param other the other primitive. Must not be null.
251     * @throws IllegalArgumentException if other is null.
252     * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
253     * @throws DataIntegrityProblemException if other is new and other.getId() != this.getId()
254     */
255    @Override
256    public void mergeFrom(OsmPrimitive other) {
257        if (!(other instanceof Node))
258            throw new IllegalArgumentException("Not a node: " + other);
259        boolean locked = writeLock();
260        try {
261            super.mergeFrom(other);
262            if (!other.isIncomplete()) {
263                setCoor(((Node) other).getCoor());
264            }
265        } finally {
266            writeUnlock(locked);
267        }
268    }
269
270    @Override
271    public void load(PrimitiveData data) {
272        if (!(data instanceof NodeData))
273            throw new IllegalArgumentException("Not a node data: " + 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
284    public NodeData save() {
285        NodeData data = new NodeData();
286        saveCommonAttributes(data);
287        if (!isIncomplete()) {
288            data.setCoor(getCoor());
289        }
290        return data;
291    }
292
293    @Override
294    public String toString() {
295        String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : "";
296        return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}';
297    }
298
299    @Override
300    public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
301        return (other instanceof Node)
302                && hasEqualSemanticFlags(other)
303                && hasEqualCoordinates((Node) other)
304                && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
305    }
306
307    private boolean hasEqualCoordinates(Node other) {
308        final LatLon c1 = getCoor();
309        final LatLon c2 = other.getCoor();
310        return (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.equalsEpsilon(c2));
311    }
312
313    @Override
314    public int compareTo(OsmPrimitive o) {
315        return o instanceof Node ? Long.compare(getUniqueId(), o.getUniqueId()) : 1;
316    }
317
318    @Override
319    public OsmPrimitiveType getType() {
320        return OsmPrimitiveType.NODE;
321    }
322
323    @Override
324    public BBox getBBox() {
325        return new BBox(lon, lat);
326    }
327
328    @Override
329    protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
330        box.add(lon, lat);
331    }
332
333    @Override
334    public void updatePosition() {
335        // Do nothing
336    }
337
338    @Override
339    public boolean isDrawable() {
340        // Not possible to draw a node without coordinates.
341        return super.isDrawable() && isLatLonKnown();
342    }
343
344    /**
345     * Check whether this node connects 2 ways.
346     *
347     * @return true if isReferredByWays(2) returns true
348     * @see #isReferredByWays(int)
349     */
350    public boolean isConnectionNode() {
351        return isReferredByWays(2);
352    }
353
354    /**
355     * Invoke to invalidate the internal cache of projected east/north coordinates.
356     * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked
357     * next time.
358     */
359    public void invalidateEastNorthCache() {
360        this.east = Double.NaN;
361        this.north = Double.NaN;
362        this.eastNorthCacheKey = null;
363    }
364
365    @Override
366    public boolean concernsArea() {
367        // A node cannot be an area
368        return false;
369    }
370
371    /**
372     * Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes
373     * matching the {@code predicate} (which may be {@code null} to consider all nodes).
374     * @param otherNodes other nodes
375     * @param hops number of hops
376     * @param predicate predicate to match
377     * @return {@code true} if {@code this} node mets the conditions
378     */
379    public boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate) {
380        CheckParameterUtil.ensureParameterNotNull(otherNodes);
381        CheckParameterUtil.ensureThat(!otherNodes.isEmpty(), "otherNodes must not be empty!");
382        CheckParameterUtil.ensureThat(hops >= 0, "hops must be non-negative!");
383        return hops == 0
384                ? isConnectedTo(otherNodes, hops, predicate, null)
385                : isConnectedTo(otherNodes, hops, predicate, new TreeSet<>());
386    }
387
388    private boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate, Set<Node> visited) {
389        if (otherNodes.contains(this)) {
390            return true;
391        }
392        if (hops > 0 && visited != null) {
393            visited.add(this);
394            for (final Way w : Utils.filteredCollection(this.getReferrers(), Way.class)) {
395                for (final Node n : w.getNodes()) {
396                    final boolean containsN = visited.contains(n);
397                    visited.add(n);
398                    if (!containsN && (predicate == null || predicate.test(n))
399                            && n.isConnectedTo(otherNodes, hops - 1, predicate, visited)) {
400                        return true;
401                    }
402                }
403            }
404        }
405        return false;
406    }
407
408    @Override
409    public boolean isOutsideDownloadArea() {
410        if (isNewOrUndeleted() || getDataSet() == null)
411            return false;
412        Area area = getDataSet().getDataSourceArea();
413        if (area == null)
414            return false;
415        LatLon coor = getCoor();
416        return coor != null && !coor.isIn(area);
417    }
418
419    /**
420     * Replies the set of referring ways.
421     * @return the set of referring ways
422     * @since 12031
423     */
424    public List<Way> getParentWays() {
425        return getFilteredList(getReferrers(), Way.class);
426    }
427}