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