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