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