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