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