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 */ 112 protected void setCoorInternal(LatLon coor, EastNorth eastNorth) { 113 if (coor != null) { 114 this.lat = coor.lat(); 115 this.lon = coor.lon(); 116 invalidateEastNorthCache(); 117 } else if (eastNorth != null) { 118 LatLon ll = Projections.inverseProject(eastNorth); 119 this.lat = ll.lat(); 120 this.lon = ll.lon(); 121 this.east = eastNorth.east(); 122 this.north = eastNorth.north(); 123 } else { 124 this.lat = Double.NaN; 125 this.lon = Double.NaN; 126 invalidateEastNorthCache(); 127 if (isVisible()) { 128 setIncomplete(true); 129 } 130 } 131 } 132 133 protected Node(long id, boolean allowNegative) { 134 super(id, allowNegative); 135 } 136 137 /** 138 * Constructs a new local {@code Node} with id 0. 139 */ 140 public Node() { 141 this(0, false); 142 } 143 144 /** 145 * Constructs an incomplete {@code Node} object with the given id. 146 * @param id The id. Must be >= 0 147 * @throws IllegalArgumentException if id < 0 148 */ 149 public Node(long id) { 150 super(id, false); 151 } 152 153 /** 154 * Constructs a new {@code Node} with the given id and version. 155 * @param id The id. Must be >= 0 156 * @param version The version 157 * @throws IllegalArgumentException if id < 0 158 */ 159 public Node(long id, int version) { 160 super(id, version, false); 161 } 162 163 /** 164 * Constructs an identical clone of the argument. 165 * @param clone The node to clone 166 * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}. 167 * If {@code false}, does nothing 168 */ 169 public Node(Node clone, boolean clearMetadata) { 170 super(clone.getUniqueId(), true /* allow negative IDs */); 171 cloneFrom(clone); 172 if (clearMetadata) { 173 clearOsmMetadata(); 174 } 175 } 176 177 /** 178 * Constructs an identical clone of the argument (including the id). 179 * @param clone The node to clone, including its id 180 */ 181 public Node(Node clone) { 182 this(clone, false); 183 } 184 185 /** 186 * Constructs a new {@code Node} with the given lat/lon with id 0. 187 * @param latlon The {@link LatLon} coordinates 188 */ 189 public Node(LatLon latlon) { 190 super(0, false); 191 setCoor(latlon); 192 } 193 194 /** 195 * Constructs a new {@code Node} with the given east/north with id 0. 196 * @param eastNorth The {@link EastNorth} coordinates 197 */ 198 public Node(EastNorth eastNorth) { 199 super(0, false); 200 setEastNorth(eastNorth); 201 } 202 203 @Override 204 void setDataset(DataSet dataSet) { 205 super.setDataset(dataSet); 206 if (!isIncomplete() && isVisible() && !isLatLonKnown()) 207 throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString()); 208 } 209 210 @Override 211 public void accept(Visitor visitor) { 212 visitor.visit(this); 213 } 214 215 @Override 216 public void accept(PrimitiveVisitor visitor) { 217 visitor.visit(this); 218 } 219 220 @Override 221 public void cloneFrom(OsmPrimitive osm) { 222 boolean locked = writeLock(); 223 try { 224 super.cloneFrom(osm); 225 setCoor(((Node) osm).getCoor()); 226 } finally { 227 writeUnlock(locked); 228 } 229 } 230 231 /** 232 * Merges the technical and semantical attributes from <code>other</code> onto this. 233 * 234 * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code> 235 * have an assigend OSM id, the IDs have to be the same. 236 * 237 * @param other the other primitive. Must not be null. 238 * @throws IllegalArgumentException if other is null. 239 * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not 240 * @throws DataIntegrityProblemException if other is new and other.getId() != this.getId() 241 */ 242 @Override 243 public void mergeFrom(OsmPrimitive other) { 244 boolean locked = writeLock(); 245 try { 246 super.mergeFrom(other); 247 if (!other.isIncomplete()) { 248 setCoor(((Node) other).getCoor()); 249 } 250 } finally { 251 writeUnlock(locked); 252 } 253 } 254 255 @Override public void load(PrimitiveData data) { 256 boolean locked = writeLock(); 257 try { 258 super.load(data); 259 setCoor(((NodeData) data).getCoor()); 260 } finally { 261 writeUnlock(locked); 262 } 263 } 264 265 @Override public NodeData save() { 266 NodeData data = new NodeData(); 267 saveCommonAttributes(data); 268 if (!isIncomplete()) { 269 data.setCoor(getCoor()); 270 } 271 return data; 272 } 273 274 @Override 275 public String toString() { 276 String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : ""; 277 return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}'; 278 } 279 280 @Override 281 public boolean hasEqualSemanticAttributes(OsmPrimitive other) { 282 if (!(other instanceof Node)) 283 return false; 284 if (!super.hasEqualSemanticAttributes(other)) 285 return false; 286 Node n = (Node) other; 287 LatLon coor = getCoor(); 288 LatLon otherCoor = n.getCoor(); 289 if (coor == null && otherCoor == null) 290 return true; 291 else if (coor != null && otherCoor != null) 292 return coor.equalsEpsilon(otherCoor); 293 else 294 return false; 295 } 296 297 @Override 298 public int compareTo(OsmPrimitive o) { 299 return o instanceof Node ? Long.compare(getUniqueId(), o.getUniqueId()) : 1; 300 } 301 302 @Override 303 public String getDisplayName(NameFormatter formatter) { 304 return formatter.format(this); 305 } 306 307 @Override 308 public OsmPrimitiveType getType() { 309 return OsmPrimitiveType.NODE; 310 } 311 312 @Override 313 public BBox getBBox() { 314 return new BBox(this); 315 } 316 317 @Override 318 public void updatePosition() { 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 /** 328 * Check whether this node connects 2 ways. 329 * 330 * @return true if isReferredByWays(2) returns true 331 * @see #isReferredByWays(int) 332 */ 333 public boolean isConnectionNode() { 334 return isReferredByWays(2); 335 } 336 337 /** 338 * Invoke to invalidate the internal cache of projected east/north coordinates. 339 * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked 340 * next time. 341 */ 342 public void invalidateEastNorthCache() { 343 this.east = Double.NaN; 344 this.north = Double.NaN; 345 } 346 347 @Override 348 public boolean concernsArea() { 349 // A node cannot be an area 350 return false; 351 } 352 353 /** 354 * Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes 355 * matching the {@code predicate} (which may be {@code null} to consider all nodes). 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<Node>()); 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) { 371 visited.add(this); 372 for (final Way w : Utils.filteredCollection(this.getReferrers(), Way.class)) { 373 for (final Node n : w.getNodes()) { 374 final boolean containsN = visited.contains(n); 375 visited.add(n); 376 if (!containsN && (predicate == null || predicate.evaluate(n)) 377 && n.isConnectedTo(otherNodes, hops - 1, predicate, visited)) { 378 return true; 379 } 380 } 381 } 382 } 383 return false; 384 } 385 386 @Override 387 public boolean isOutsideDownloadArea() { 388 return !isNewOrUndeleted() && getDataSet() != null && getDataSet().getDataSourceArea() != null 389 && getCoor() != null && !getCoor().isIn(getDataSet().getDataSourceArea()); 390 } 391}