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