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