// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm;
import java.awt.geom.Area;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.data.projection.Projection;
import org.openstreetmap.josm.data.projection.Projections;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.Utils;
/**
* One node data, consisting of one world coordinate waypoint.
*
* @author imi
*/
public final class Node extends OsmPrimitive implements INode {
/*
* We "inline" lat/lon rather than using a LatLon-object => reduces memory footprint
*/
private double lat = Double.NaN;
private double lon = Double.NaN;
/*
* the cached projected coordinates
*/
private double east = Double.NaN;
private double north = Double.NaN;
/**
* The cache key to use for {@link #east} and {@link #north}.
*/
private Object eastNorthCacheKey;
/**
* Determines if this node has valid coordinates.
* @return {@code true} if this node has valid coordinates
* @since 7828
*/
public boolean isLatLonKnown() {
return !Double.isNaN(lat) && !Double.isNaN(lon);
}
@Override
public void setCoor(LatLon coor) {
updateCoor(coor, null);
}
@Override
public void setEastNorth(EastNorth eastNorth) {
updateCoor(null, eastNorth);
}
private void updateCoor(LatLon coor, EastNorth eastNorth) {
if (getDataSet() != null) {
boolean locked = writeLock();
try {
getDataSet().fireNodeMoved(this, coor, eastNorth);
} finally {
writeUnlock(locked);
}
} else {
setCoorInternal(coor, eastNorth);
}
}
/**
* Returns lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
* @return lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()}
*/
@Override
public LatLon getCoor() {
if (!isLatLonKnown()) return null;
return new LatLon(lat, lon);
}
/**
* <p>Replies the projected east/north coordinates.</p>
*
* <p>Uses the {@link Main#getProjection() global projection} to project the lan/lon-coordinates.
* Internally caches the projected coordinates.</p>
*
* <p>Replies {@code null} if this node doesn't know lat/lon-coordinates, i.e. because it is an incomplete node.
*
* @return the east north coordinates or {@code null}
* @see #invalidateEastNorthCache()
*
*/
@Override
public EastNorth getEastNorth() {
return getEastNorth(Main.getProjection());
}
/**
* Replies the projected east/north coordinates.
* <p>
* The result of the last conversion is cached. The cache object is used as cache key.
* @param projection The projection to use.
* @return The projected east/north coordinates
* @since 10827
*/
public EastNorth getEastNorth(Projection projection) {
if (!isLatLonKnown()) return null;
if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(projection.getCacheKey(), eastNorthCacheKey)) {
// projected coordinates haven't been calculated yet,
// so fill the cache of the projected node coordinates
EastNorth en = Projections.project(new LatLon(lat, lon));
this.east = en.east();
this.north = en.north();
this.eastNorthCacheKey = projection.getCacheKey();
}
return new EastNorth(east, north);
}
/**
* To be used only by Dataset.reindexNode
* @param coor lat/lon
* @param eastNorth east/north
*/
void setCoorInternal(LatLon coor, EastNorth eastNorth) {
if (coor != null) {
this.lat = coor.lat();
this.lon = coor.lon();
invalidateEastNorthCache();
} else if (eastNorth != null) {
LatLon ll = Projections.inverseProject(eastNorth);
this.lat = ll.lat();
this.lon = ll.lon();
this.east = eastNorth.east();
this.north = eastNorth.north();
this.eastNorthCacheKey = Main.getProjection().getCacheKey();
} else {
this.lat = Double.NaN;
this.lon = Double.NaN;
invalidateEastNorthCache();
if (isVisible()) {
setIncomplete(true);
}
}
}
protected Node(long id, boolean allowNegative) {
super(id, allowNegative);
}
/**
* Constructs a new local {@code Node} with id 0.
*/
public Node() {
this(0, false);
}
/**
* Constructs an incomplete {@code Node} object with the given id.
* @param id The id. Must be >= 0
* @throws IllegalArgumentException if id < 0
*/
public Node(long id) {
super(id, false);
}
/**
* Constructs a new {@code Node} with the given id and version.
* @param id The id. Must be >= 0
* @param version The version
* @throws IllegalArgumentException if id < 0
*/
public Node(long id, int version) {
super(id, version, false);
}
/**
* Constructs an identical clone of the argument.
* @param clone The node to clone
* @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
* If {@code false}, does nothing
*/
public Node(Node clone, boolean clearMetadata) {
super(clone.getUniqueId(), true /* allow negative IDs */);
cloneFrom(clone);
if (clearMetadata) {
clearOsmMetadata();
}
}
/**
* Constructs an identical clone of the argument (including the id).
* @param clone The node to clone, including its id
*/
public Node(Node clone) {
this(clone, false);
}
/**
* Constructs a new {@code Node} with the given lat/lon with id 0.
* @param latlon The {@link LatLon} coordinates
*/
public Node(LatLon latlon) {
super(0, false);
setCoor(latlon);
}
/**
* Constructs a new {@code Node} with the given east/north with id 0.
* @param eastNorth The {@link EastNorth} coordinates
*/
public Node(EastNorth eastNorth) {
super(0, false);
setEastNorth(eastNorth);
}
@Override
void setDataset(DataSet dataSet) {
super.setDataset(dataSet);
if (!isIncomplete() && isVisible() && !isLatLonKnown())
throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString());
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
@Override
public void accept(PrimitiveVisitor visitor) {
visitor.visit(this);
}
@Override
public void cloneFrom(OsmPrimitive osm) {
if (!(osm instanceof Node))
throw new IllegalArgumentException("Not a node: " + osm);
boolean locked = writeLock();
try {
super.cloneFrom(osm);
setCoor(((Node) osm).getCoor());
} finally {
writeUnlock(locked);
}
}
/**
* Merges the technical and semantical attributes from <code>other</code> onto this.
*
* Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
* have an assigend OSM id, the IDs have to be the same.
*
* @param other the other primitive. Must not be null.
* @throws IllegalArgumentException if other is null.
* @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
* @throws DataIntegrityProblemException if other is new and other.getId() != this.getId()
*/
@Override
public void mergeFrom(OsmPrimitive other) {
if (!(other instanceof Node))
throw new IllegalArgumentException("Not a node: " + other);
boolean locked = writeLock();
try {
super.mergeFrom(other);
if (!other.isIncomplete()) {
setCoor(((Node) other).getCoor());
}
} finally {
writeUnlock(locked);
}
}
@Override
public void load(PrimitiveData data) {
if (!(data instanceof NodeData))
throw new IllegalArgumentException("Not a node data: " + data);
boolean locked = writeLock();
try {
super.load(data);
setCoor(((NodeData) data).getCoor());
} finally {
writeUnlock(locked);
}
}
@Override
public NodeData save() {
NodeData data = new NodeData();
saveCommonAttributes(data);
if (!isIncomplete()) {
data.setCoor(getCoor());
}
return data;
}
@Override
public String toString() {
String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : "";
return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}';
}
@Override
public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
return (other instanceof Node)
&& hasEqualSemanticFlags(other)
&& hasEqualCoordinates((Node) other)
&& super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
}
private boolean hasEqualCoordinates(Node other) {
final LatLon c1 = getCoor();
final LatLon c2 = other.getCoor();
return (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.equalsEpsilon(c2));
}
@Override
public int compareTo(OsmPrimitive o) {
return o instanceof Node ? Long.compare(getUniqueId(), o.getUniqueId()) : 1;
}
@Override
public String getDisplayName(NameFormatter formatter) {
return formatter.format(this);
}
@Override
public OsmPrimitiveType getType() {
return OsmPrimitiveType.NODE;
}
@Override
public BBox getBBox() {
return new BBox(lon, lat);
}
@Override
protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
box.add(lon, lat);
}
@Override
public void updatePosition() {
// Do nothing
}
@Override
public boolean isDrawable() {
// Not possible to draw a node without coordinates.
return super.isDrawable() && isLatLonKnown();
}
/**
* Check whether this node connects 2 ways.
*
* @return true if isReferredByWays(2) returns true
* @see #isReferredByWays(int)
*/
public boolean isConnectionNode() {
return isReferredByWays(2);
}
/**
* Invoke to invalidate the internal cache of projected east/north coordinates.
* Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked
* next time.
*/
public void invalidateEastNorthCache() {
this.east = Double.NaN;
this.north = Double.NaN;
this.eastNorthCacheKey = null;
}
@Override
public boolean concernsArea() {
// A node cannot be an area
return false;
}
/**
* Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes
* matching the {@code predicate} (which may be {@code null} to consider all nodes).
* @param otherNodes other nodes
* @param hops number of hops
* @param predicate predicate to match
* @return {@code true} if {@code this} node mets the conditions
*/
public boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate) {
CheckParameterUtil.ensureParameterNotNull(otherNodes);
CheckParameterUtil.ensureThat(!otherNodes.isEmpty(), "otherNodes must not be empty!");
CheckParameterUtil.ensureThat(hops >= 0, "hops must be non-negative!");
return hops == 0
? isConnectedTo(otherNodes, hops, predicate, null)
: isConnectedTo(otherNodes, hops, predicate, new TreeSet<Node>());
}
private boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate, Set<Node> visited) {
if (otherNodes.contains(this)) {
return true;
}
if (hops > 0 && visited != null) {
visited.add(this);
for (final Way w : Utils.filteredCollection(this.getReferrers(), Way.class)) {
for (final Node n : w.getNodes()) {
final boolean containsN = visited.contains(n);
visited.add(n);
if (!containsN && (predicate == null || predicate.test(n))
&& n.isConnectedTo(otherNodes, hops - 1, predicate, visited)) {
return true;
}
}
}
}
return false;
}
@Override
public boolean isOutsideDownloadArea() {
if (isNewOrUndeleted() || getDataSet() == null)
return false;
Area area = getDataSet().getDataSourceArea();
if (area == null)
return false;
LatLon coor = getCoor();
return coor != null && !coor.isIn(area);
}
}