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 final boolean isLatLonKnown() { 043 return !Double.isNaN(lat) && !Double.isNaN(lon); 044 } 045 046 @Override 047 public final void setCoor(LatLon coor) { 048 updateCoor(coor, null); 049 } 050 051 @Override 052 public final 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 final 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 final 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) throws IllegalArgumentException { 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) throws IllegalArgumentException { 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}. 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(Visitor 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 boolean locked = writeLock(); 222 try { 223 super.cloneFrom(osm); 224 setCoor(((Node)osm).getCoor()); 225 } finally { 226 writeUnlock(locked); 227 } 228 } 229 230 /** 231 * Merges the technical and semantical attributes from <code>other</code> onto this. 232 * 233 * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code> 234 * have an assigend OSM id, the IDs have to be the same. 235 * 236 * @param other the other primitive. Must not be null. 237 * @throws IllegalArgumentException thrown if other is null. 238 * @throws DataIntegrityProblemException thrown if either this is new and other is not, or other is new and this is not 239 * @throws DataIntegrityProblemException thrown if other is new and other.getId() != this.getId() 240 */ 241 @Override 242 public void mergeFrom(OsmPrimitive other) { 243 boolean locked = writeLock(); 244 try { 245 super.mergeFrom(other); 246 if (!other.isIncomplete()) { 247 setCoor(((Node)other).getCoor()); 248 } 249 } finally { 250 writeUnlock(locked); 251 } 252 } 253 254 @Override public void load(PrimitiveData data) { 255 boolean locked = writeLock(); 256 try { 257 super.load(data); 258 setCoor(((NodeData)data).getCoor()); 259 } finally { 260 writeUnlock(locked); 261 } 262 } 263 264 @Override public NodeData save() { 265 NodeData data = new NodeData(); 266 saveCommonAttributes(data); 267 if (!isIncomplete()) { 268 data.setCoor(getCoor()); 269 } 270 return data; 271 } 272 273 @Override 274 public String toString() { 275 String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : ""; 276 return "{Node id=" + getUniqueId() + " version=" + getVersion() + " " + getFlagsAsString() + " " + coorDesc+"}"; 277 } 278 279 @Override 280 public boolean hasEqualSemanticAttributes(OsmPrimitive other) { 281 if (!(other instanceof Node)) 282 return false; 283 if (! super.hasEqualSemanticAttributes(other)) 284 return false; 285 Node n = (Node)other; 286 LatLon coor = getCoor(); 287 LatLon otherCoor = n.getCoor(); 288 if (coor == null && otherCoor == null) 289 return true; 290 else if (coor != null && otherCoor != null) 291 return coor.equalsEpsilon(otherCoor); 292 else 293 return false; 294 } 295 296 @Override 297 public int compareTo(OsmPrimitive o) { 298 return o instanceof Node ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : 1; 299 } 300 301 @Override 302 public String getDisplayName(NameFormatter formatter) { 303 return formatter.format(this); 304 } 305 306 @Override 307 public OsmPrimitiveType getType() { 308 return OsmPrimitiveType.NODE; 309 } 310 311 @Override 312 public BBox getBBox() { 313 return new BBox(this); 314 } 315 316 @Override 317 public void updatePosition() { 318 } 319 320 @Override 321 public boolean isDrawable() { 322 // Not possible to draw a node without coordinates. 323 return super.isDrawable() && isLatLonKnown(); 324 } 325 326 /** 327 * Check whether this node connects 2 ways. 328 * 329 * @return true if isReferredByWays(2) returns true 330 * @see #isReferredByWays(int) 331 */ 332 public boolean isConnectionNode() { 333 return isReferredByWays(2); 334 } 335 336 /** 337 * Invoke to invalidate the internal cache of projected east/north coordinates. 338 * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked 339 * next time. 340 */ 341 public void invalidateEastNorthCache() { 342 this.east = Double.NaN; 343 this.north = Double.NaN; 344 } 345 346 @Override 347 public boolean concernsArea() { 348 // A node cannot be an area 349 return false; 350 } 351 352 /** 353 * Tests whether {@code this} node is connected to {@code otherNode} via at most {@code hops} nodes 354 * matching the {@code predicate} (which may be {@code null} to consider all nodes). 355 */ 356 public boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate) { 357 CheckParameterUtil.ensureParameterNotNull(otherNodes); 358 CheckParameterUtil.ensureThat(!otherNodes.isEmpty(), "otherNodes must not be empty!"); 359 CheckParameterUtil.ensureThat(hops >= 0, "hops must be non-negative!"); 360 return hops == 0 361 ? isConnectedTo(otherNodes, hops, predicate, null) 362 : isConnectedTo(otherNodes, hops, predicate, new TreeSet<Node>()); 363 } 364 365 private boolean isConnectedTo(final Collection<Node> otherNodes, final int hops, Predicate<Node> predicate, Set<Node> visited) { 366 if (otherNodes.contains(this)) { 367 return true; 368 } 369 if (hops > 0) { 370 visited.add(this); 371 for (final Way w : Utils.filteredCollection(this.getReferrers(), Way.class)) { 372 for (final Node n : w.getNodes()) { 373 final boolean containsN = visited.contains(n); 374 visited.add(n); 375 if (!containsN && (predicate == null || predicate.evaluate(n)) && n.isConnectedTo(otherNodes, hops - 1, predicate, visited)) { 376 return true; 377 } 378 } 379 } 380 } 381 return false; 382 } 383 384 @Override 385 public boolean isOutsideDownloadArea() { 386 return !isNewOrUndeleted() && getDataSet() != null && getDataSet().getDataSourceArea() != null 387 && !getCoor().isIn(getDataSet().getDataSourceArea()); 388 } 389}