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