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