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 final boolean isLatLonKnown() {
043        return !Double.isNaN(lat) && !Double.isNaN(lon);
044    }
045
046    @Override
047    public final void setCoor(LatLon coor) {
048        updateCoor(coor, null);
049    }
050
051    @Override
052    public final 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 final 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 final 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     */
112    protected void setCoorInternal(LatLon coor, EastNorth eastNorth) {
113        if (coor != null) {
114            this.lat = coor.lat();
115            this.lon = coor.lon();
116            invalidateEastNorthCache();
117        } else if (eastNorth != null) {
118            LatLon ll = Projections.inverseProject(eastNorth);
119            this.lat = ll.lat();
120            this.lon = ll.lon();
121            this.east = eastNorth.east();
122            this.north = eastNorth.north();
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 &gt;= 0
147     * @throws IllegalArgumentException if id &lt; 0
148     */
149    public Node(long id) throws IllegalArgumentException {
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 &gt;= 0
156     * @param version The version
157     * @throws IllegalArgumentException if id &lt; 0
158     */
159    public Node(long id, int version) throws IllegalArgumentException {
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}. 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(Visitor 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        boolean locked = writeLock();
222        try {
223            super.cloneFrom(osm);
224            setCoor(((Node)osm).getCoor());
225        } finally {
226            writeUnlock(locked);
227        }
228    }
229
230    /**
231     * Merges the technical and semantical attributes from <code>other</code> onto this.
232     *
233     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
234     * have an assigend OSM id, the IDs have to be the same.
235     *
236     * @param other the other primitive. Must not be null.
237     * @throws IllegalArgumentException thrown if other is null.
238     * @throws DataIntegrityProblemException thrown if either this is new and other is not, or other is new and this is not
239     * @throws DataIntegrityProblemException thrown if other is new and other.getId() != this.getId()
240     */
241    @Override
242    public void mergeFrom(OsmPrimitive other) {
243        boolean locked = writeLock();
244        try {
245            super.mergeFrom(other);
246            if (!other.isIncomplete()) {
247                setCoor(((Node)other).getCoor());
248            }
249        } finally {
250            writeUnlock(locked);
251        }
252    }
253
254    @Override public void load(PrimitiveData data) {
255        boolean locked = writeLock();
256        try {
257            super.load(data);
258            setCoor(((NodeData)data).getCoor());
259        } finally {
260            writeUnlock(locked);
261        }
262    }
263
264    @Override public NodeData save() {
265        NodeData data = new NodeData();
266        saveCommonAttributes(data);
267        if (!isIncomplete()) {
268            data.setCoor(getCoor());
269        }
270        return data;
271    }
272
273    @Override
274    public String toString() {
275        String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : "";
276        return "{Node id=" + getUniqueId() + " version=" + getVersion() + " " + getFlagsAsString() + " "  + coorDesc+"}";
277    }
278
279    @Override
280    public boolean hasEqualSemanticAttributes(OsmPrimitive other) {
281        if (!(other instanceof Node))
282            return false;
283        if (! super.hasEqualSemanticAttributes(other))
284            return false;
285        Node n = (Node)other;
286        LatLon coor = getCoor();
287        LatLon otherCoor = n.getCoor();
288        if (coor == null && otherCoor == null)
289            return true;
290        else if (coor != null && otherCoor != null)
291            return coor.equalsEpsilon(otherCoor);
292        else
293            return false;
294    }
295
296    @Override
297    public int compareTo(OsmPrimitive o) {
298        return o instanceof Node ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : 1;
299    }
300
301    @Override
302    public String getDisplayName(NameFormatter formatter) {
303        return formatter.format(this);
304    }
305
306    @Override
307    public OsmPrimitiveType getType() {
308        return OsmPrimitiveType.NODE;
309    }
310
311    @Override
312    public BBox getBBox() {
313        return new BBox(this);
314    }
315
316    @Override
317    public void updatePosition() {
318    }
319
320    @Override
321    public boolean isDrawable() {
322        // Not possible to draw a node without coordinates.
323        return super.isDrawable() && isLatLonKnown();
324    }
325
326    /**
327     * Check whether this node connects 2 ways.
328     *
329     * @return true if isReferredByWays(2) returns true
330     * @see #isReferredByWays(int)
331     */
332    public boolean isConnectionNode() {
333        return isReferredByWays(2);
334    }
335
336    /**
337     * Invoke to invalidate the internal cache of projected east/north coordinates.
338     * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked
339     * next time.
340     */
341    public void invalidateEastNorthCache() {
342        this.east = Double.NaN;
343        this.north = Double.NaN;
344    }
345
346    @Override
347    public boolean concernsArea() {
348        // A node cannot be an area
349        return false;
350    }
351
352    /**
353     * Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes
354     * matching the {@code predicate} (which may be {@code null} to consider all nodes).
355     */
356    public boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate) {
357        CheckParameterUtil.ensureParameterNotNull(otherNodes);
358        CheckParameterUtil.ensureThat(!otherNodes.isEmpty(), "otherNodes must not be empty!");
359        CheckParameterUtil.ensureThat(hops >= 0, "hops must be non-negative!");
360        return hops == 0
361                ? isConnectedTo(otherNodes, hops, predicate, null)
362                : isConnectedTo(otherNodes, hops, predicate, new TreeSet<Node>());
363    }
364
365    private boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate, Set<Node> visited) {
366        if (otherNodes.contains(this)) {
367            return true;
368        }
369        if (hops > 0) {
370            visited.add(this);
371            for (final Way w : Utils.filteredCollection(this.getReferrers(), Way.class)) {
372                for (final Node n : w.getNodes()) {
373                    final boolean containsN = visited.contains(n);
374                    visited.add(n);
375                    if (!containsN && (predicate == null || predicate.evaluate(n)) && n.isConnectedTo(otherNodes, hops - 1, predicate, visited)) {
376                        return true;
377                    }
378                }
379            }
380        }
381        return false;
382    }
383
384    @Override
385    public boolean isOutsideDownloadArea() {
386        return !isNewOrUndeleted() && getDataSet() != null && getDataSet().getDataSourceArea() != null
387                && !getCoor().isIn(getDataSet().getDataSourceArea());
388    }
389}