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