001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.awt.geom.Area;
005import java.util.List;
006import java.util.Objects;
007import java.util.Set;
008import java.util.stream.Collectors;
009
010import org.openstreetmap.josm.data.Bounds;
011import org.openstreetmap.josm.data.coor.EastNorth;
012import org.openstreetmap.josm.data.coor.LatLon;
013import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
014import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
015import org.openstreetmap.josm.data.projection.Projecting;
016import org.openstreetmap.josm.data.projection.ProjectionRegistry;
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    static final UniqueIdGenerator idGenerator = new UniqueIdGenerator();
026
027    /*
028     * We "inline" lat/lon rather than using a LatLon-object => reduces memory footprint
029     */
030    private double lat = Double.NaN;
031    private double lon = Double.NaN;
032
033    /*
034     * the cached projected coordinates
035     */
036    private double east = Double.NaN;
037    private double north = Double.NaN;
038    /**
039     * The cache key to use for {@link #east} and {@link #north}.
040     */
041    private Object eastNorthCacheKey;
042
043    @Override
044    public void setCoor(LatLon coor) {
045        updateCoor(coor, null);
046    }
047
048    @Override
049    public void setEastNorth(EastNorth eastNorth) {
050        updateCoor(null, eastNorth);
051    }
052
053    private void updateCoor(LatLon coor, EastNorth eastNorth) {
054        if (getDataSet() != null) {
055            boolean locked = writeLock();
056            try {
057                getDataSet().fireNodeMoved(this, coor, eastNorth);
058            } finally {
059                writeUnlock(locked);
060            }
061        } else {
062            setCoorInternal(coor, eastNorth);
063        }
064    }
065
066    /**
067     * Returns lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
068     * @return lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
069     */
070    @Override
071    public LatLon getCoor() {
072        if (!isLatLonKnown()) {
073            return null;
074        } else {
075            return new LatLon(lat, lon);
076        }
077    }
078
079    @Override
080    public double lat() {
081        return lat;
082    }
083
084    @Override
085    public double lon() {
086        return lon;
087    }
088
089    @Override
090    public EastNorth getEastNorth(Projecting projection) {
091        if (!isLatLonKnown()) return null;
092
093        if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(projection.getCacheKey(), eastNorthCacheKey)) {
094            // projected coordinates haven't been calculated yet,
095            // so fill the cache of the projected node coordinates
096            EastNorth en = projection.latlon2eastNorth(this);
097            this.east = en.east();
098            this.north = en.north();
099            this.eastNorthCacheKey = projection.getCacheKey();
100        }
101        return new EastNorth(east, north);
102    }
103
104    /**
105     * To be used only by Dataset.reindexNode
106     * @param coor lat/lon
107     * @param eastNorth east/north
108     */
109    void setCoorInternal(LatLon coor, EastNorth eastNorth) {
110        if (coor != null) {
111            this.lat = coor.lat();
112            this.lon = coor.lon();
113            invalidateEastNorthCache();
114        } else if (eastNorth != null) {
115            LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth);
116            this.lat = ll.lat();
117            this.lon = ll.lon();
118            this.east = eastNorth.east();
119            this.north = eastNorth.north();
120            this.eastNorthCacheKey = ProjectionRegistry.getProjection().getCacheKey();
121        } else {
122            this.lat = Double.NaN;
123            this.lon = Double.NaN;
124            invalidateEastNorthCache();
125            if (isVisible()) {
126                setIncomplete(true);
127            }
128        }
129    }
130
131    Node(long id, boolean allowNegative) {
132        super(id, allowNegative);
133    }
134
135    /**
136     * Constructs a new local {@code Node} with id 0.
137     */
138    public Node() {
139        this(0, false);
140    }
141
142    /**
143     * Constructs an incomplete {@code Node} object with the given id.
144     * @param id The id. Must be >= 0
145     * @throws IllegalArgumentException if id < 0
146     */
147    public Node(long id) {
148        super(id, false);
149    }
150
151    /**
152     * Constructs a new {@code Node} with the given id and version.
153     * @param id The id. Must be >= 0
154     * @param version The version
155     * @throws IllegalArgumentException if id < 0
156     */
157    public Node(long id, int version) {
158        super(id, version, false);
159    }
160
161    /**
162     * Constructs an identical clone of the argument.
163     * @param clone The node to clone
164     * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
165     * If {@code false}, does nothing
166     */
167    public Node(Node clone, boolean clearMetadata) {
168        super(clone.getUniqueId(), true /* allow negative IDs */);
169        cloneFrom(clone);
170        if (clearMetadata) {
171            clearOsmMetadata();
172        }
173    }
174
175    /**
176     * Constructs an identical clone of the argument (including the id).
177     * @param clone The node to clone, including its id
178     */
179    public Node(Node clone) {
180        this(clone, false);
181    }
182
183    /**
184     * Constructs a new {@code Node} with the given lat/lon with id 0.
185     * @param latlon The {@link LatLon} coordinates
186     */
187    public Node(LatLon latlon) {
188        super(0, false);
189        setCoor(latlon);
190    }
191
192    /**
193     * Constructs a new {@code Node} with the given east/north with id 0.
194     * @param eastNorth The {@link EastNorth} coordinates
195     */
196    public Node(EastNorth eastNorth) {
197        super(0, false);
198        setEastNorth(eastNorth);
199    }
200
201    @Override
202    void setDataset(DataSet dataSet) {
203        super.setDataset(dataSet);
204        if (!isIncomplete() && isVisible() && !isLatLonKnown())
205            throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString());
206    }
207
208    @Override
209    public void accept(OsmPrimitiveVisitor visitor) {
210        visitor.visit(this);
211    }
212
213    @Override
214    public void accept(PrimitiveVisitor visitor) {
215        visitor.visit(this);
216    }
217
218    @Override
219    public void cloneFrom(OsmPrimitive osm, boolean copyChildren) {
220        if (!(osm instanceof Node))
221            throw new IllegalArgumentException("Not a node: " + osm);
222        boolean locked = writeLock();
223        try {
224            super.cloneFrom(osm, copyChildren);
225            setCoor(((Node) osm).getCoor());
226        } finally {
227            writeUnlock(locked);
228        }
229    }
230
231    /**
232     * Merges the technical and semantical attributes from <code>other</code> onto this.
233     *
234     * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
235     * have an assigend OSM id, the IDs have to be the same.
236     *
237     * @param other the other primitive. Must not be null.
238     * @throws IllegalArgumentException if other is null.
239     * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
240     * @throws DataIntegrityProblemException if other is new and other.getId() != this.getId()
241     */
242    @Override
243    public void mergeFrom(OsmPrimitive other) {
244        if (!(other instanceof Node))
245            throw new IllegalArgumentException("Not a node: " + 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
258    public void load(PrimitiveData data) {
259        if (!(data instanceof NodeData))
260            throw new IllegalArgumentException("Not a node data: " + data);
261        boolean locked = writeLock();
262        try {
263            super.load(data);
264            setCoor(((NodeData) data).getCoor());
265        } finally {
266            writeUnlock(locked);
267        }
268    }
269
270    @Override
271    public NodeData save() {
272        NodeData data = new NodeData();
273        saveCommonAttributes(data);
274        if (!isIncomplete()) {
275            data.setCoor(getCoor());
276        }
277        return data;
278    }
279
280    @Override
281    public String toString() {
282        String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : "";
283        return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}';
284    }
285
286    @Override
287    public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
288        return (other instanceof Node)
289                && hasEqualSemanticFlags(other)
290                && hasEqualCoordinates((Node) other)
291                && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
292    }
293
294    private boolean hasEqualCoordinates(Node other) {
295        final LatLon c1 = getCoor();
296        final LatLon c2 = other.getCoor();
297        return (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.equalsEpsilon(c2));
298    }
299
300    @Override
301    public OsmPrimitiveType getType() {
302        return OsmPrimitiveType.NODE;
303    }
304
305    @Override
306    public BBox getBBox() {
307        return new BBox(lon, lat);
308    }
309
310    @Override
311    protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
312        box.add(lon, lat);
313    }
314
315    @Override
316    public void updatePosition() {
317        // Do nothing
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    @Override
327    public boolean isReferredByWays(int n) {
328        return isNodeReferredByWays(n);
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        this.eastNorthCacheKey = null;
340    }
341
342    @Override
343    public boolean concernsArea() {
344        // A node cannot be an area
345        return false;
346    }
347
348    @Override
349    public boolean isOutsideDownloadArea() {
350        if (isNewOrUndeleted() || getDataSet() == null)
351            return false;
352        Area area = getDataSet().getDataSourceArea();
353        if (area == null)
354            return false;
355        LatLon coor = getCoor();
356        return coor != null && !coor.isIn(area);
357    }
358
359    /**
360     * Replies the set of referring ways.
361     * @return the set of referring ways
362     * @since 12031
363     */
364    public List<Way> getParentWays() {
365        return referrers(Way.class).collect(Collectors.toList());
366    }
367
368    /**
369     * Determines if this node is outside of the world. See also #13538.
370     * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon and east/north
371     * @since 14960
372     */
373    public boolean isOutSideWorld() {
374        LatLon ll = getCoor();
375        if (ll != null) {
376            Bounds b = ProjectionRegistry.getProjection().getWorldBoundsLatLon();
377            if (lat() < b.getMinLat() || lat() > b.getMaxLat() || lon() < b.getMinLon() || lon() > b.getMaxLon()) {
378                return true;
379            }
380            if (!ProjectionRegistry.getProjection().latlon2eastNorth(ll).equalsEpsilon(getEastNorth(), 1.0)) {
381                // we get here if a node was moved or created left from -180 or right from +180
382                return true;
383            }
384        }
385        return false;
386    }
387
388    @Override
389    public UniqueIdGenerator getIdGenerator() {
390        return idGenerator;
391    }
392
393    @Override
394    protected void updateDirectionFlags() {
395        // Nodes do not need/have a direction, greatly improves performance, see #18886
396    }
397}