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