001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.data.osm; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.util.ArrayList; 007 import java.util.Arrays; 008 import java.util.List; 009 import java.util.Set; 010 import java.util.HashSet; 011 012 import org.openstreetmap.josm.Main; 013 import org.openstreetmap.josm.data.coor.LatLon; 014 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 015 import org.openstreetmap.josm.data.osm.visitor.Visitor; 016 import org.openstreetmap.josm.gui.DefaultNameFormatter; 017 import org.openstreetmap.josm.tools.CopyList; 018 import org.openstreetmap.josm.tools.Pair; 019 020 /** 021 * One full way, consisting of a list of way {@link Node nodes}. 022 * 023 * @author imi 024 * @since 64 025 */ 026 public final class Way extends OsmPrimitive implements IWay { 027 028 /** 029 * All way nodes in this way 030 * 031 */ 032 private Node[] nodes = new Node[0]; 033 private BBox bbox; 034 035 /** 036 * 037 * You can modify returned list but changes will not be propagated back 038 * to the Way. Use {@link #setNodes(List)} to update this way 039 * @return Nodes composing the way 040 * @since 1862 041 */ 042 public List<Node> getNodes() { 043 return new CopyList<Node>(nodes); 044 } 045 046 /** 047 * Set new list of nodes to way. This method is preferred to multiple calls to addNode/removeNode 048 * and similar methods because nodes are internally saved as array which means lower memory overhead 049 * but also slower modifying operations. 050 * @param nodes New way nodes. Can be null, in that case all way nodes are removed 051 * @since 1862 052 */ 053 public void setNodes(List<Node> nodes) { 054 boolean locked = writeLock(); 055 try { 056 for (Node node:this.nodes) { 057 node.removeReferrer(this); 058 } 059 060 if (nodes == null) { 061 this.nodes = new Node[0]; 062 } else { 063 this.nodes = nodes.toArray(new Node[nodes.size()]); 064 } 065 for (Node node: this.nodes) { 066 node.addReferrer(this); 067 } 068 069 clearCachedStyle(); 070 fireNodesChanged(); 071 } finally { 072 writeUnlock(locked); 073 } 074 } 075 076 /** 077 * Prevent directly following identical nodes in ways. 078 */ 079 private List<Node> removeDouble(List<Node> nodes) { 080 Node last = null; 081 int count = nodes.size(); 082 for(int i = 0; i < count && count > 2;) { 083 Node n = nodes.get(i); 084 if(last == n) { 085 nodes.remove(i); 086 --count; 087 } else { 088 last = n; 089 ++i; 090 } 091 } 092 return nodes; 093 } 094 095 /** 096 * Replies the number of nodes in this ways. 097 * 098 * @return the number of nodes in this ways. 099 * @since 1862 100 */ 101 @Override 102 public int getNodesCount() { 103 return nodes.length; 104 } 105 106 /** 107 * Replies the node at position <code>index</code>. 108 * 109 * @param index the position 110 * @return the node at position <code>index</code> 111 * @exception IndexOutOfBoundsException thrown if <code>index</code> < 0 112 * or <code>index</code> >= {@link #getNodesCount()} 113 * @since 1862 114 */ 115 public Node getNode(int index) { 116 return nodes[index]; 117 } 118 119 @Override 120 public long getNodeId(int idx) { 121 return nodes[idx].getUniqueId(); 122 } 123 124 /** 125 * Replies true if this way contains the node <code>node</code>, false 126 * otherwise. Replies false if <code>node</code> is null. 127 * 128 * @param node the node. May be null. 129 * @return true if this way contains the node <code>node</code>, false 130 * otherwise 131 * @since 1911 132 */ 133 public boolean containsNode(Node node) { 134 if (node == null) return false; 135 136 Node[] nodes = this.nodes; 137 for (int i=0; i<nodes.length; i++) { 138 if (nodes[i].equals(node)) 139 return true; 140 } 141 return false; 142 } 143 144 /** 145 * Return nodes adjacent to <code>node</code> 146 * 147 * @param node the node. May be null. 148 * @return Set of nodes adjacent to <code>node</code> 149 * @since 4671 150 */ 151 public Set<Node> getNeighbours(Node node) { 152 HashSet<Node> neigh = new HashSet<Node>(); 153 154 if (node == null) return neigh; 155 156 Node[] nodes = this.nodes; 157 for (int i=0; i<nodes.length; i++) { 158 if (nodes[i].equals(node)) { 159 if (i > 0) 160 neigh.add(nodes[i-1]); 161 if (i < nodes.length-1) 162 neigh.add(nodes[i+1]); 163 } 164 } 165 return neigh; 166 } 167 168 /** 169 * Replies the ordered {@link List} of chunks of this way. Each chunk is replied as a {@link Pair} of {@link Node nodes}. 170 * @param sort If true, the nodes of each pair are sorted as defined by {@link Pair#sort}. 171 * If false, Pair.a and Pair.b are in the way order (i.e for a given Pair(n), Pair(n-1).b == Pair(n).a, Pair(n).b == Pair(n+1).a, etc.) 172 * @return The ordered list of chunks of this way. 173 * @since 3348 174 */ 175 public List<Pair<Node,Node>> getNodePairs(boolean sort) { 176 List<Pair<Node,Node>> chunkSet = new ArrayList<Pair<Node,Node>>(); 177 if (isIncomplete()) return chunkSet; 178 Node lastN = null; 179 Node[] nodes = this.nodes; 180 for (Node n : nodes) { 181 if (lastN == null) { 182 lastN = n; 183 continue; 184 } 185 Pair<Node,Node> np = new Pair<Node,Node>(lastN, n); 186 if (sort) { 187 Pair.sort(np); 188 } 189 chunkSet.add(np); 190 lastN = n; 191 } 192 return chunkSet; 193 } 194 195 @Override public void visit(Visitor visitor) { 196 visitor.visit(this); 197 } 198 199 @Override public void visit(PrimitiveVisitor visitor) { 200 visitor.visit(this); 201 } 202 203 protected Way(long id, boolean allowNegative) { 204 super(id, allowNegative); 205 } 206 207 /** 208 * Contructs a new {@code Way} with id 0. 209 * @since 86 210 */ 211 public Way() { 212 super(0, false); 213 } 214 215 /** 216 * Contructs a new {@code Way} from an existing {@code Way}. 217 * @param original The original {@code Way} to be identically cloned. Must not be null 218 * @param clearId If true, clears the OSM id as defined by {@link #clearOsmId}. If false, does nothing 219 * @since 2410 220 */ 221 public Way(Way original, boolean clearId) { 222 super(original.getUniqueId(), true); 223 cloneFrom(original); 224 if (clearId) { 225 clearOsmId(); 226 } 227 } 228 229 /** 230 * Contructs a new {@code Way} from an existing {@code Way} (including its id). 231 * @param original The original {@code Way} to be identically cloned. Must not be null 232 * @since 86 233 */ 234 public Way(Way original) { 235 this(original, false); 236 } 237 238 /** 239 * Contructs a new {@code Way} for the given id. If the id > 0, the way is marked 240 * as incomplete. If id == 0 then way is marked as new 241 * 242 * @param id the id. >= 0 required 243 * @throws IllegalArgumentException if id < 0 244 * @since 343 245 */ 246 public Way(long id) throws IllegalArgumentException { 247 super(id, false); 248 } 249 250 /** 251 * Contructs a new {@code Way} with given id and version. 252 * @param id the id. >= 0 required 253 * @param version the version 254 * @throws IllegalArgumentException if id < 0 255 * @since 2620 256 */ 257 public Way(long id, int version) throws IllegalArgumentException { 258 super(id, version, false); 259 } 260 261 @Override 262 public void load(PrimitiveData data) { 263 boolean locked = writeLock(); 264 try { 265 super.load(data); 266 267 WayData wayData = (WayData) data; 268 269 List<Node> newNodes = new ArrayList<Node>(wayData.getNodes().size()); 270 for (Long nodeId : wayData.getNodes()) { 271 Node node = (Node)getDataSet().getPrimitiveById(nodeId, OsmPrimitiveType.NODE); 272 if (node != null) { 273 newNodes.add(node); 274 } else 275 throw new AssertionError("Data consistency problem - way with missing node detected"); 276 } 277 setNodes(newNodes); 278 } finally { 279 writeUnlock(locked); 280 } 281 } 282 283 @Override 284 public WayData save() { 285 WayData data = new WayData(); 286 saveCommonAttributes(data); 287 for (Node node:nodes) { 288 data.getNodes().add(node.getUniqueId()); 289 } 290 return data; 291 } 292 293 @Override 294 public void cloneFrom(OsmPrimitive osm) { 295 boolean locked = writeLock(); 296 try { 297 super.cloneFrom(osm); 298 Way otherWay = (Way)osm; 299 setNodes(otherWay.getNodes()); 300 } finally { 301 writeUnlock(locked); 302 } 303 } 304 305 @Override 306 public String toString() { 307 String nodesDesc = isIncomplete()?"(incomplete)":"nodes=" + Arrays.toString(nodes); 308 return "{Way id=" + getUniqueId() + " version=" + getVersion()+ " " + getFlagsAsString() + " " + nodesDesc + "}"; 309 } 310 311 @Override 312 public boolean hasEqualSemanticAttributes(OsmPrimitive other) { 313 if (other == null || ! (other instanceof Way) ) 314 return false; 315 if (! super.hasEqualSemanticAttributes(other)) 316 return false; 317 Way w = (Way)other; 318 if (getNodesCount() != w.getNodesCount()) return false; 319 for (int i=0;i<getNodesCount();i++) { 320 if (! getNode(i).hasEqualSemanticAttributes(w.getNode(i))) 321 return false; 322 } 323 return true; 324 } 325 326 @Override 327 public int compareTo(OsmPrimitive o) { 328 if (o instanceof Relation) 329 return 1; 330 return o instanceof Way ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : -1; 331 } 332 333 /** 334 * Removes the given {@link Node} from this way. Ignored, if n is null. 335 * @param n The node to remove. Ignored, if null 336 * @since 1463 337 */ 338 public void removeNode(Node n) { 339 if (n == null || isIncomplete()) return; 340 boolean locked = writeLock(); 341 try { 342 boolean closed = (lastNode() == n && firstNode() == n); 343 int i; 344 List<Node> copy = getNodes(); 345 while ((i = copy.indexOf(n)) >= 0) { 346 copy.remove(i); 347 } 348 i = copy.size(); 349 if (closed && i > 2) { 350 copy.add(copy.get(0)); 351 } else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) { 352 copy.remove(i-1); 353 } 354 setNodes(removeDouble(copy)); 355 } finally { 356 writeUnlock(locked); 357 } 358 } 359 360 /** 361 * Removes the given set of {@link Node nodes} from this way. Ignored, if selection is null. 362 * @param selection The selection of nodes to remove. Ignored, if null 363 * @since 5408 364 */ 365 public void removeNodes(Set<? extends Node> selection) { 366 if (selection == null || isIncomplete()) return; 367 boolean locked = writeLock(); 368 try { 369 boolean closed = (lastNode() == firstNode() && selection.contains(lastNode())); 370 List<Node> copy = new ArrayList<Node>(); 371 372 for (Node n: nodes) { 373 if (!selection.contains(n)) { 374 copy.add(n); 375 } 376 } 377 378 int i = copy.size(); 379 if (closed && i > 2) { 380 copy.add(copy.get(0)); 381 } else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) { 382 copy.remove(i-1); 383 } 384 setNodes(removeDouble(copy)); 385 } finally { 386 writeUnlock(locked); 387 } 388 } 389 390 /** 391 * Adds a node to the end of the list of nodes. Ignored, if n is null. 392 * 393 * @param n the node. Ignored, if null 394 * @throws IllegalStateException thrown, if this way is marked as incomplete. We can't add a node 395 * to an incomplete way 396 * @since 1313 397 */ 398 public void addNode(Node n) throws IllegalStateException { 399 if (n==null) return; 400 401 boolean locked = writeLock(); 402 try { 403 if (isIncomplete()) 404 throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId())); 405 clearCachedStyle(); 406 n.addReferrer(this); 407 Node[] newNodes = new Node[nodes.length + 1]; 408 System.arraycopy(nodes, 0, newNodes, 0, nodes.length); 409 newNodes[nodes.length] = n; 410 nodes = newNodes; 411 fireNodesChanged(); 412 } finally { 413 writeUnlock(locked); 414 } 415 } 416 417 /** 418 * Adds a node at position offs. 419 * 420 * @param offs the offset 421 * @param n the node. Ignored, if null. 422 * @throws IllegalStateException thrown, if this way is marked as incomplete. We can't add a node 423 * to an incomplete way 424 * @throws IndexOutOfBoundsException thrown if offs is out of bounds 425 * @since 1313 426 */ 427 public void addNode(int offs, Node n) throws IllegalStateException, IndexOutOfBoundsException { 428 if (n==null) return; 429 430 boolean locked = writeLock(); 431 try { 432 if (isIncomplete()) 433 throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId())); 434 435 clearCachedStyle(); 436 n.addReferrer(this); 437 Node[] newNodes = new Node[nodes.length + 1]; 438 System.arraycopy(nodes, 0, newNodes, 0, offs); 439 System.arraycopy(nodes, offs, newNodes, offs + 1, nodes.length - offs); 440 newNodes[offs] = n; 441 nodes = newNodes; 442 fireNodesChanged(); 443 } finally { 444 writeUnlock(locked); 445 } 446 } 447 448 @Override 449 public void setDeleted(boolean deleted) { 450 boolean locked = writeLock(); 451 try { 452 for (Node n:nodes) { 453 if (deleted) { 454 n.removeReferrer(this); 455 } else { 456 n.addReferrer(this); 457 } 458 } 459 fireNodesChanged(); 460 super.setDeleted(deleted); 461 } finally { 462 writeUnlock(locked); 463 } 464 } 465 466 @Override 467 public boolean isClosed() { 468 if (isIncomplete()) return false; 469 470 Node[] nodes = this.nodes; 471 return nodes.length >= 3 && nodes[nodes.length-1] == nodes[0]; 472 } 473 474 /** 475 * Determines if this way denotes an area (closed way with at least three distinct nodes). 476 * @return {@code true} if this way is closed and contains at least three distinct nodes 477 * @see #isClosed 478 * @since 5490 479 */ 480 public boolean isArea() { 481 if (this.nodes.length >= 4 && isClosed()) { 482 Node distinctNode = null; 483 for (int i=1; i<nodes.length-1; i++) { 484 if (distinctNode == null && nodes[i] != nodes[0]) { 485 distinctNode = nodes[i]; 486 } else if (distinctNode != null && nodes[i] != nodes[0] && nodes[i] != distinctNode) { 487 return true; 488 } 489 } 490 } 491 return false; 492 } 493 494 /** 495 * Returns the last node of this way. 496 * The result equals <tt>{@link #getNode getNode}({@link #getNodesCount getNodesCount} - 1)</tt>. 497 * @return the last node of this way 498 * @since 1400 499 */ 500 public Node lastNode() { 501 Node[] nodes = this.nodes; 502 if (isIncomplete() || nodes.length == 0) return null; 503 return nodes[nodes.length-1]; 504 } 505 506 /** 507 * Returns the first node of this way. 508 * The result equals {@link #getNode getNode}{@code (0)}. 509 * @return the first node of this way 510 * @since 1400 511 */ 512 public Node firstNode() { 513 Node[] nodes = this.nodes; 514 if (isIncomplete() || nodes.length == 0) return null; 515 return nodes[0]; 516 } 517 518 /** 519 * Replies true if the given node is the first or the last one of this way, false otherwise. 520 * @param n The node to test 521 * @return true if the {@code n} is the first or the last node, false otherwise. 522 * @since 1400 523 */ 524 public boolean isFirstLastNode(Node n) { 525 Node[] nodes = this.nodes; 526 if (isIncomplete() || nodes.length == 0) return false; 527 return n == nodes[0] || n == nodes[nodes.length -1]; 528 } 529 530 /** 531 * Replies true if the given node is an inner node of this way, false otherwise. 532 * @param n The node to test 533 * @return true if the {@code n} is an inner node, false otherwise. 534 * @since 3515 535 */ 536 public boolean isInnerNode(Node n) { 537 Node[] nodes = this.nodes; 538 if (isIncomplete() || nodes.length <= 2) return false; 539 /* circular ways have only inner nodes, so return true for them! */ 540 if (n == nodes[0] && n == nodes[nodes.length-1]) return true; 541 for (int i = 1; i < nodes.length - 1; ++i) { 542 if (nodes[i] == n) return true; 543 } 544 return false; 545 } 546 547 @Override 548 public String getDisplayName(NameFormatter formatter) { 549 return formatter.format(this); 550 } 551 552 @Override 553 public OsmPrimitiveType getType() { 554 return OsmPrimitiveType.WAY; 555 } 556 557 @Override 558 public OsmPrimitiveType getDisplayType() { 559 return isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY; 560 } 561 562 private void checkNodes() { 563 DataSet dataSet = getDataSet(); 564 if (dataSet != null) { 565 Node[] nodes = this.nodes; 566 for (Node n: nodes) { 567 if (n.getDataSet() != dataSet) 568 throw new DataIntegrityProblemException("Nodes in way must be in the same dataset", 569 tr("Nodes in way must be in the same dataset")); 570 if (n.isDeleted()) 571 throw new DataIntegrityProblemException("Deleted node referenced: " + toString(), 572 "<html>" + tr("Deleted node referenced by {0}", DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>"); 573 } 574 if (Main.pref.getBoolean("debug.checkNullCoor", true)) { 575 for (Node n: nodes) { 576 if (n.isVisible() && !n.isIncomplete() && (n.getCoor() == null || n.getEastNorth() == null)) 577 throw new DataIntegrityProblemException("Complete visible node with null coordinates: " + toString() + n.get3892DebugInfo(), 578 "<html>" + tr("Complete node {0} with null coordinates in way {1}", 579 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(n), 580 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>"); 581 } 582 } 583 } 584 } 585 586 private void fireNodesChanged() { 587 checkNodes(); 588 if (getDataSet() != null) { 589 getDataSet().fireWayNodesChanged(this); 590 } 591 } 592 593 @Override 594 public void setDataset(DataSet dataSet) { 595 super.setDataset(dataSet); 596 checkNodes(); 597 } 598 599 @Override 600 public BBox getBBox() { 601 if (getDataSet() == null) 602 return new BBox(this); 603 if (bbox == null) { 604 bbox = new BBox(this); 605 } 606 return new BBox(bbox); 607 } 608 609 @Override 610 public void updatePosition() { 611 bbox = new BBox(this); 612 } 613 614 /** 615 * Replies true if this way has incomplete nodes, false otherwise. 616 * @return true if this way has incomplete nodes, false otherwise. 617 * @since 2587 618 */ 619 public boolean hasIncompleteNodes() { 620 Node[] nodes = this.nodes; 621 for (Node node : nodes) { 622 if (node.isIncomplete()) 623 return true; 624 } 625 return false; 626 } 627 628 @Override 629 public boolean isUsable() { 630 return super.isUsable() && !hasIncompleteNodes(); 631 } 632 633 @Override 634 public boolean isDrawable() { 635 return super.isDrawable() && !hasIncompleteNodes(); 636 } 637 638 /** 639 * Replies the length of the way, in metres, as computed by {@link LatLon#greatCircleDistance}. 640 * @return The length of the way, in metres 641 * @since 4138 642 */ 643 public double getLength() { 644 double length = 0; 645 Node lastN = null; 646 for (Node n:nodes) { 647 if (lastN != null) { 648 LatLon lastNcoor = lastN.getCoor(); 649 LatLon coor = n.getCoor(); 650 if (lastNcoor != null && coor != null) { 651 length += coor.greatCircleDistance(lastNcoor); 652 } 653 } 654 lastN = n; 655 } 656 return length; 657 } 658 659 /** 660 * Tests if this way is a oneway. 661 * @return {@code 1} if the way is a oneway, 662 * {@code -1} if the way is a reversed oneway, 663 * {@code 0} otherwise. 664 * @since 5199 665 */ 666 public int isOneway() { 667 String oneway = get("oneway"); 668 if (oneway != null) { 669 if ("-1".equals(oneway)) { 670 return -1; 671 } else { 672 Boolean isOneway = OsmUtils.getOsmBoolean(oneway); 673 if (isOneway != null && isOneway) { 674 return 1; 675 } 676 } 677 } 678 return 0; 679 } 680 681 /** 682 * Replies the first node of this way, respecting or not its oneway state. 683 * @param respectOneway If true and if this way is a oneway, replies the last node. Otherwise, replies the first node. 684 * @return the first node of this way, according to {@code respectOneway} and its oneway state. 685 * @since 5199 686 */ 687 public Node firstNode(boolean respectOneway) { 688 return !respectOneway || isOneway() != -1 ? firstNode() : lastNode(); 689 } 690 691 /** 692 * Replies the last node of this way, respecting or not its oneway state. 693 * @param respectOneway If true and if this way is a oneway, replies the first node. Otherwise, replies the last node. 694 * @return the last node of this way, according to {@code respectOneway} and its oneway state. 695 * @since 5199 696 */ 697 public Node lastNode(boolean respectOneway) { 698 return !respectOneway || isOneway() != -1 ? lastNode() : firstNode(); 699 } 700 }