001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm; 003 004import java.awt.geom.Area; 005import java.util.Collection; 006import java.util.List; 007import java.util.Objects; 008import java.util.Set; 009import java.util.TreeSet; 010import java.util.function.Predicate; 011import java.util.stream.Collectors; 012 013import org.openstreetmap.josm.data.Bounds; 014import org.openstreetmap.josm.data.coor.EastNorth; 015import org.openstreetmap.josm.data.coor.LatLon; 016import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 017import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 018import org.openstreetmap.josm.data.projection.Projecting; 019import org.openstreetmap.josm.data.projection.ProjectionRegistry; 020import org.openstreetmap.josm.tools.CheckParameterUtil; 021 022/** 023 * One node data, consisting of one world coordinate waypoint. 024 * 025 * @author imi 026 */ 027public final class Node extends OsmPrimitive implements INode { 028 029 /* 030 * We "inline" lat/lon rather than using a LatLon-object => reduces memory footprint 031 */ 032 private double lat = Double.NaN; 033 private double lon = Double.NaN; 034 035 /* 036 * the cached projected coordinates 037 */ 038 private double east = Double.NaN; 039 private double north = Double.NaN; 040 /** 041 * The cache key to use for {@link #east} and {@link #north}. 042 */ 043 private Object eastNorthCacheKey; 044 045 @Override 046 public void setCoor(LatLon coor) { 047 updateCoor(coor, null); 048 } 049 050 @Override 051 public void setEastNorth(EastNorth eastNorth) { 052 updateCoor(null, eastNorth); 053 } 054 055 private void updateCoor(LatLon coor, EastNorth eastNorth) { 056 if (getDataSet() != null) { 057 boolean locked = writeLock(); 058 try { 059 getDataSet().fireNodeMoved(this, coor, eastNorth); 060 } finally { 061 writeUnlock(locked); 062 } 063 } else { 064 setCoorInternal(coor, eastNorth); 065 } 066 } 067 068 /** 069 * Returns lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()} 070 * @return lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()} 071 */ 072 @Override 073 public LatLon getCoor() { 074 if (!isLatLonKnown()) { 075 return null; 076 } else { 077 return new LatLon(lat, lon); 078 } 079 } 080 081 @Override 082 public double lat() { 083 return lat; 084 } 085 086 @Override 087 public double lon() { 088 return lon; 089 } 090 091 @Override 092 public EastNorth getEastNorth(Projecting projection) { 093 if (!isLatLonKnown()) return null; 094 095 if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(projection.getCacheKey(), eastNorthCacheKey)) { 096 // projected coordinates haven't been calculated yet, 097 // so fill the cache of the projected node coordinates 098 EastNorth en = projection.latlon2eastNorth(this); 099 this.east = en.east(); 100 this.north = en.north(); 101 this.eastNorthCacheKey = projection.getCacheKey(); 102 } 103 return new EastNorth(east, north); 104 } 105 106 /** 107 * To be used only by Dataset.reindexNode 108 * @param coor lat/lon 109 * @param eastNorth east/north 110 */ 111 void setCoorInternal(LatLon coor, EastNorth eastNorth) { 112 if (coor != null) { 113 this.lat = coor.lat(); 114 this.lon = coor.lon(); 115 invalidateEastNorthCache(); 116 } else if (eastNorth != null) { 117 LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth); 118 this.lat = ll.lat(); 119 this.lon = ll.lon(); 120 this.east = eastNorth.east(); 121 this.north = eastNorth.north(); 122 this.eastNorthCacheKey = ProjectionRegistry.getProjection().getCacheKey(); 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 >= 0 147 * @throws IllegalArgumentException if id < 0 148 */ 149 public Node(long id) { 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 >= 0 156 * @param version The version 157 * @throws IllegalArgumentException if id < 0 158 */ 159 public Node(long id, int version) { 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}. 167 * If {@code false}, does nothing 168 */ 169 public Node(Node clone, boolean clearMetadata) { 170 super(clone.getUniqueId(), true /* allow negative IDs */); 171 cloneFrom(clone); 172 if (clearMetadata) { 173 clearOsmMetadata(); 174 } 175 } 176 177 /** 178 * Constructs an identical clone of the argument (including the id). 179 * @param clone The node to clone, including its id 180 */ 181 public Node(Node clone) { 182 this(clone, false); 183 } 184 185 /** 186 * Constructs a new {@code Node} with the given lat/lon with id 0. 187 * @param latlon The {@link LatLon} coordinates 188 */ 189 public Node(LatLon latlon) { 190 super(0, false); 191 setCoor(latlon); 192 } 193 194 /** 195 * Constructs a new {@code Node} with the given east/north with id 0. 196 * @param eastNorth The {@link EastNorth} coordinates 197 */ 198 public Node(EastNorth eastNorth) { 199 super(0, false); 200 setEastNorth(eastNorth); 201 } 202 203 @Override 204 void setDataset(DataSet dataSet) { 205 super.setDataset(dataSet); 206 if (!isIncomplete() && isVisible() && !isLatLonKnown()) 207 throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString()); 208 } 209 210 @Override 211 public void accept(OsmPrimitiveVisitor visitor) { 212 visitor.visit(this); 213 } 214 215 @Override 216 public void accept(PrimitiveVisitor visitor) { 217 visitor.visit(this); 218 } 219 220 @Override 221 public void cloneFrom(OsmPrimitive osm) { 222 if (!(osm instanceof Node)) 223 throw new IllegalArgumentException("Not a node: " + 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 if (!(other instanceof Node)) 247 throw new IllegalArgumentException("Not a node: " + other); 248 boolean locked = writeLock(); 249 try { 250 super.mergeFrom(other); 251 if (!other.isIncomplete()) { 252 setCoor(((Node) other).getCoor()); 253 } 254 } finally { 255 writeUnlock(locked); 256 } 257 } 258 259 @Override 260 public void load(PrimitiveData data) { 261 if (!(data instanceof NodeData)) 262 throw new IllegalArgumentException("Not a node data: " + data); 263 boolean locked = writeLock(); 264 try { 265 super.load(data); 266 setCoor(((NodeData) data).getCoor()); 267 } finally { 268 writeUnlock(locked); 269 } 270 } 271 272 @Override 273 public NodeData save() { 274 NodeData data = new NodeData(); 275 saveCommonAttributes(data); 276 if (!isIncomplete()) { 277 data.setCoor(getCoor()); 278 } 279 return data; 280 } 281 282 @Override 283 public String toString() { 284 String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : ""; 285 return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}'; 286 } 287 288 @Override 289 public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) { 290 return (other instanceof Node) 291 && hasEqualSemanticFlags(other) 292 && hasEqualCoordinates((Node) other) 293 && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly); 294 } 295 296 private boolean hasEqualCoordinates(Node other) { 297 final LatLon c1 = getCoor(); 298 final LatLon c2 = other.getCoor(); 299 return (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.equalsEpsilon(c2)); 300 } 301 302 @Override 303 public OsmPrimitiveType getType() { 304 return OsmPrimitiveType.NODE; 305 } 306 307 @Override 308 public BBox getBBox() { 309 return new BBox(lon, lat); 310 } 311 312 @Override 313 protected void addToBBox(BBox box, Set<PrimitiveId> visited) { 314 box.add(lon, lat); 315 } 316 317 @Override 318 public void updatePosition() { 319 // Do nothing 320 } 321 322 @Override 323 public boolean isDrawable() { 324 // Not possible to draw a node without coordinates. 325 return super.isDrawable() && isLatLonKnown(); 326 } 327 328 @Override 329 public boolean isReferredByWays(int n) { 330 return isNodeReferredByWays(n); 331 } 332 333 /** 334 * Invoke to invalidate the internal cache of projected east/north coordinates. 335 * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked 336 * next time. 337 */ 338 public void invalidateEastNorthCache() { 339 this.east = Double.NaN; 340 this.north = Double.NaN; 341 this.eastNorthCacheKey = null; 342 } 343 344 @Override 345 public boolean concernsArea() { 346 // A node cannot be an area 347 return false; 348 } 349 350 /** 351 * Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes 352 * matching the {@code predicate} (which may be {@code null} to consider all nodes). 353 * @param otherNodes other nodes 354 * @param hops number of hops 355 * @param predicate predicate to match 356 * @return {@code true} if {@code this} node mets the conditions 357 */ 358 public boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate) { 359 CheckParameterUtil.ensureParameterNotNull(otherNodes); 360 CheckParameterUtil.ensureThat(!otherNodes.isEmpty(), "otherNodes must not be empty!"); 361 CheckParameterUtil.ensureThat(hops >= 0, "hops must be non-negative!"); 362 return hops == 0 363 ? isConnectedTo(otherNodes, hops, predicate, null) 364 : isConnectedTo(otherNodes, hops, predicate, new TreeSet<>()); 365 } 366 367 private boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate, Set<Node> visited) { 368 if (otherNodes.contains(this)) { 369 return true; 370 } 371 if (hops > 0 && visited != null) { 372 visited.add(this); 373 for (final Way w : getParentWays()) { 374 for (final Node n : w.getNodes()) { 375 final boolean containsN = visited.contains(n); 376 if (!containsN && (predicate == null || predicate.test(n)) 377 && n.isConnectedTo(otherNodes, hops - 1, predicate, visited)) { 378 return true; 379 } 380 } 381 } 382 } 383 return false; 384 } 385 386 @Override 387 public boolean isOutsideDownloadArea() { 388 if (isNewOrUndeleted() || getDataSet() == null) 389 return false; 390 Area area = getDataSet().getDataSourceArea(); 391 if (area == null) 392 return false; 393 LatLon coor = getCoor(); 394 return coor != null && !coor.isIn(area); 395 } 396 397 /** 398 * Replies the set of referring ways. 399 * @return the set of referring ways 400 * @since 12031 401 */ 402 public List<Way> getParentWays() { 403 return referrers(Way.class).collect(Collectors.toList()); 404 } 405 406 /** 407 * Determines if this node is outside of the world. See also #13538. 408 * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon and east/north 409 * @since 14960 410 */ 411 public boolean isOutSideWorld() { 412 LatLon ll = getCoor(); 413 if (ll != null) { 414 Bounds b = ProjectionRegistry.getProjection().getWorldBoundsLatLon(); 415 if (lat() < b.getMinLat() || lat() > b.getMaxLat() || lon() < b.getMinLon() || lon() > b.getMaxLon()) { 416 return true; 417 } 418 if (!ProjectionRegistry.getProjection().latlon2eastNorth(ll).equalsEpsilon(getEastNorth(), 1.0)) { 419 // we get here if a node was moved or created left from -180 or right from +180 420 return true; 421 } 422 } 423 return false; 424 } 425}