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