001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm; 003 004import java.awt.geom.Area; 005import java.util.List; 006import java.util.Objects; 007import java.util.Set; 008import java.util.stream.Collectors; 009 010import org.openstreetmap.josm.data.Bounds; 011import org.openstreetmap.josm.data.coor.EastNorth; 012import org.openstreetmap.josm.data.coor.LatLon; 013import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 014import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 015import org.openstreetmap.josm.data.projection.Projecting; 016import org.openstreetmap.josm.data.projection.ProjectionRegistry; 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 static final UniqueIdGenerator idGenerator = new UniqueIdGenerator(); 026 027 /* 028 * We "inline" lat/lon rather than using a LatLon-object => reduces memory footprint 029 */ 030 private double lat = Double.NaN; 031 private double lon = Double.NaN; 032 033 /* 034 * the cached projected coordinates 035 */ 036 private double east = Double.NaN; 037 private double north = Double.NaN; 038 /** 039 * The cache key to use for {@link #east} and {@link #north}. 040 */ 041 private Object eastNorthCacheKey; 042 043 @Override 044 public void setCoor(LatLon coor) { 045 updateCoor(coor, null); 046 } 047 048 @Override 049 public void setEastNorth(EastNorth eastNorth) { 050 updateCoor(null, eastNorth); 051 } 052 053 private void updateCoor(LatLon coor, EastNorth eastNorth) { 054 if (getDataSet() != null) { 055 boolean locked = writeLock(); 056 try { 057 getDataSet().fireNodeMoved(this, coor, eastNorth); 058 } finally { 059 writeUnlock(locked); 060 } 061 } else { 062 setCoorInternal(coor, eastNorth); 063 } 064 } 065 066 /** 067 * Returns lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()} 068 * @return lat/lon coordinates of this node, or {@code null} unless {@link #isLatLonKnown()} 069 */ 070 @Override 071 public LatLon getCoor() { 072 if (!isLatLonKnown()) { 073 return null; 074 } else { 075 return new LatLon(lat, lon); 076 } 077 } 078 079 @Override 080 public double lat() { 081 return lat; 082 } 083 084 @Override 085 public double lon() { 086 return lon; 087 } 088 089 @Override 090 public EastNorth getEastNorth(Projecting projection) { 091 if (!isLatLonKnown()) return null; 092 093 if (Double.isNaN(east) || Double.isNaN(north) || !Objects.equals(projection.getCacheKey(), eastNorthCacheKey)) { 094 // projected coordinates haven't been calculated yet, 095 // so fill the cache of the projected node coordinates 096 EastNorth en = projection.latlon2eastNorth(this); 097 this.east = en.east(); 098 this.north = en.north(); 099 this.eastNorthCacheKey = projection.getCacheKey(); 100 } 101 return new EastNorth(east, north); 102 } 103 104 /** 105 * To be used only by Dataset.reindexNode 106 * @param coor lat/lon 107 * @param eastNorth east/north 108 */ 109 void setCoorInternal(LatLon coor, EastNorth eastNorth) { 110 if (coor != null) { 111 this.lat = coor.lat(); 112 this.lon = coor.lon(); 113 invalidateEastNorthCache(); 114 } else if (eastNorth != null) { 115 LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth); 116 this.lat = ll.lat(); 117 this.lon = ll.lon(); 118 this.east = eastNorth.east(); 119 this.north = eastNorth.north(); 120 this.eastNorthCacheKey = ProjectionRegistry.getProjection().getCacheKey(); 121 } else { 122 this.lat = Double.NaN; 123 this.lon = Double.NaN; 124 invalidateEastNorthCache(); 125 if (isVisible()) { 126 setIncomplete(true); 127 } 128 } 129 } 130 131 Node(long id, boolean allowNegative) { 132 super(id, allowNegative); 133 } 134 135 /** 136 * Constructs a new local {@code Node} with id 0. 137 */ 138 public Node() { 139 this(0, false); 140 } 141 142 /** 143 * Constructs an incomplete {@code Node} object with the given id. 144 * @param id The id. Must be >= 0 145 * @throws IllegalArgumentException if id < 0 146 */ 147 public Node(long id) { 148 super(id, false); 149 } 150 151 /** 152 * Constructs a new {@code Node} with the given id and version. 153 * @param id The id. Must be >= 0 154 * @param version The version 155 * @throws IllegalArgumentException if id < 0 156 */ 157 public Node(long id, int version) { 158 super(id, version, false); 159 } 160 161 /** 162 * Constructs an identical clone of the argument. 163 * @param clone The node to clone 164 * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}. 165 * If {@code false}, does nothing 166 */ 167 public Node(Node clone, boolean clearMetadata) { 168 super(clone.getUniqueId(), true /* allow negative IDs */); 169 cloneFrom(clone); 170 if (clearMetadata) { 171 clearOsmMetadata(); 172 } 173 } 174 175 /** 176 * Constructs an identical clone of the argument (including the id). 177 * @param clone The node to clone, including its id 178 */ 179 public Node(Node clone) { 180 this(clone, false); 181 } 182 183 /** 184 * Constructs a new {@code Node} with the given lat/lon with id 0. 185 * @param latlon The {@link LatLon} coordinates 186 */ 187 public Node(LatLon latlon) { 188 super(0, false); 189 setCoor(latlon); 190 } 191 192 /** 193 * Constructs a new {@code Node} with the given east/north with id 0. 194 * @param eastNorth The {@link EastNorth} coordinates 195 */ 196 public Node(EastNorth eastNorth) { 197 super(0, false); 198 setEastNorth(eastNorth); 199 } 200 201 @Override 202 void setDataset(DataSet dataSet) { 203 super.setDataset(dataSet); 204 if (!isIncomplete() && isVisible() && !isLatLonKnown()) 205 throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString()); 206 } 207 208 @Override 209 public void accept(OsmPrimitiveVisitor visitor) { 210 visitor.visit(this); 211 } 212 213 @Override 214 public void accept(PrimitiveVisitor visitor) { 215 visitor.visit(this); 216 } 217 218 @Override 219 public void cloneFrom(OsmPrimitive osm, boolean copyChildren) { 220 if (!(osm instanceof Node)) 221 throw new IllegalArgumentException("Not a node: " + osm); 222 boolean locked = writeLock(); 223 try { 224 super.cloneFrom(osm, copyChildren); 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 if (!(other instanceof Node)) 245 throw new IllegalArgumentException("Not a node: " + 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 258 public void load(PrimitiveData data) { 259 if (!(data instanceof NodeData)) 260 throw new IllegalArgumentException("Not a node data: " + data); 261 boolean locked = writeLock(); 262 try { 263 super.load(data); 264 setCoor(((NodeData) data).getCoor()); 265 } finally { 266 writeUnlock(locked); 267 } 268 } 269 270 @Override 271 public NodeData save() { 272 NodeData data = new NodeData(); 273 saveCommonAttributes(data); 274 if (!isIncomplete()) { 275 data.setCoor(getCoor()); 276 } 277 return data; 278 } 279 280 @Override 281 public String toString() { 282 String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : ""; 283 return "{Node id=" + getUniqueId() + " version=" + getVersion() + ' ' + getFlagsAsString() + ' ' + coorDesc+'}'; 284 } 285 286 @Override 287 public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) { 288 return (other instanceof Node) 289 && hasEqualSemanticFlags(other) 290 && hasEqualCoordinates((Node) other) 291 && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly); 292 } 293 294 private boolean hasEqualCoordinates(Node other) { 295 final LatLon c1 = getCoor(); 296 final LatLon c2 = other.getCoor(); 297 return (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.equalsEpsilon(c2)); 298 } 299 300 @Override 301 public OsmPrimitiveType getType() { 302 return OsmPrimitiveType.NODE; 303 } 304 305 @Override 306 public BBox getBBox() { 307 return new BBox(lon, lat); 308 } 309 310 @Override 311 protected void addToBBox(BBox box, Set<PrimitiveId> visited) { 312 box.add(lon, lat); 313 } 314 315 @Override 316 public void updatePosition() { 317 // Do nothing 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 @Override 327 public boolean isReferredByWays(int n) { 328 return isNodeReferredByWays(n); 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 this.eastNorthCacheKey = null; 340 } 341 342 @Override 343 public boolean concernsArea() { 344 // A node cannot be an area 345 return false; 346 } 347 348 @Override 349 public boolean isOutsideDownloadArea() { 350 if (isNewOrUndeleted() || getDataSet() == null) 351 return false; 352 Area area = getDataSet().getDataSourceArea(); 353 if (area == null) 354 return false; 355 LatLon coor = getCoor(); 356 return coor != null && !coor.isIn(area); 357 } 358 359 /** 360 * Replies the set of referring ways. 361 * @return the set of referring ways 362 * @since 12031 363 */ 364 public List<Way> getParentWays() { 365 return referrers(Way.class).collect(Collectors.toList()); 366 } 367 368 /** 369 * Determines if this node is outside of the world. See also #13538. 370 * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon and east/north 371 * @since 14960 372 */ 373 public boolean isOutSideWorld() { 374 LatLon ll = getCoor(); 375 if (ll != null) { 376 Bounds b = ProjectionRegistry.getProjection().getWorldBoundsLatLon(); 377 if (lat() < b.getMinLat() || lat() > b.getMaxLat() || lon() < b.getMinLon() || lon() > b.getMaxLon()) { 378 return true; 379 } 380 if (!ProjectionRegistry.getProjection().latlon2eastNorth(ll).equalsEpsilon(getEastNorth(), 1.0)) { 381 // we get here if a node was moved or created left from -180 or right from +180 382 return true; 383 } 384 } 385 return false; 386 } 387 388 @Override 389 public UniqueIdGenerator getIdGenerator() { 390 return idGenerator; 391 } 392 393 @Override 394 protected void updateDirectionFlags() { 395 // Nodes do not need/have a direction, greatly improves performance, see #18886 396 } 397}