source: josm/trunk/src/org/openstreetmap/josm/data/osm/Way.java@ 13915

Last change on this file since 13915 was 13907, checked in by Don-vip, 6 years ago

add IWay.setNodes()

  • Property svn:eol-style set to native
File size: 24.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.HashSet;
9import java.util.List;
10import java.util.Map;
11import java.util.Set;
12import java.util.stream.Collectors;
13
14import org.openstreetmap.josm.data.coor.LatLon;
15import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
16import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
17import org.openstreetmap.josm.spi.preferences.Config;
18import org.openstreetmap.josm.tools.CopyList;
19import org.openstreetmap.josm.tools.Geometry;
20import org.openstreetmap.josm.tools.Pair;
21import org.openstreetmap.josm.tools.Utils;
22
23/**
24 * One full way, consisting of a list of way {@link Node nodes}.
25 *
26 * @author imi
27 * @since 64
28 */
29public final class Way extends OsmPrimitive implements IWay<Node> {
30
31 /**
32 * All way nodes in this way
33 */
34 private Node[] nodes = new Node[0];
35 private BBox bbox;
36
37 @Override
38 public List<Node> getNodes() {
39 return new CopyList<>(nodes);
40 }
41
42 @Override
43 public void setNodes(List<Node> nodes) {
44 checkDatasetNotReadOnly();
45 boolean locked = writeLock();
46 try {
47 for (Node node:this.nodes) {
48 node.removeReferrer(this);
49 node.clearCachedStyle();
50 }
51
52 if (nodes == null) {
53 this.nodes = new Node[0];
54 } else {
55 this.nodes = nodes.toArray(new Node[0]);
56 }
57 for (Node node: this.nodes) {
58 node.addReferrer(this);
59 node.clearCachedStyle();
60 }
61
62 clearCachedStyle();
63 fireNodesChanged();
64 } finally {
65 writeUnlock(locked);
66 }
67 }
68
69 /**
70 * Prevent directly following identical nodes in ways.
71 * @param nodes list of nodes
72 * @return {@code nodes} with consecutive identical nodes removed
73 */
74 private static List<Node> removeDouble(List<Node> nodes) {
75 Node last = null;
76 int count = nodes.size();
77 for (int i = 0; i < count && count > 2;) {
78 Node n = nodes.get(i);
79 if (last == n) {
80 nodes.remove(i);
81 --count;
82 } else {
83 last = n;
84 ++i;
85 }
86 }
87 return nodes;
88 }
89
90 @Override
91 public int getNodesCount() {
92 return nodes.length;
93 }
94
95 @Override
96 public Node getNode(int index) {
97 return nodes[index];
98 }
99
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 &gt; 0, the way is marked
228 * as incomplete. If id == 0 then way is marked as new
229 *
230 * @param id the id. &gt;= 0 required
231 * @throws IllegalArgumentException if id &lt; 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. &gt;= 0 required
241 * @param version the version
242 * @throws IllegalArgumentException if id &lt; 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 /**
493 * Returns the last node of this way.
494 * The result equals <code>{@link #getNode getNode}({@link #getNodesCount getNodesCount} - 1)</code>.
495 * @return the last node of this way
496 * @since 1400
497 */
498 public Node lastNode() {
499 Node[] nodes = this.nodes;
500 if (isIncomplete() || nodes.length == 0) return null;
501 return nodes[nodes.length-1];
502 }
503
504 /**
505 * Returns the first node of this way.
506 * The result equals {@link #getNode getNode}{@code (0)}.
507 * @return the first node of this way
508 * @since 1400
509 */
510 public Node firstNode() {
511 Node[] nodes = this.nodes;
512 if (isIncomplete() || nodes.length == 0) return null;
513 return nodes[0];
514 }
515
516 /**
517 * Replies true if the given node is the first or the last one of this way, false otherwise.
518 * @param n The node to test
519 * @return true if the {@code n} is the first or the last node, false otherwise.
520 * @since 1400
521 */
522 public boolean isFirstLastNode(Node n) {
523 Node[] nodes = this.nodes;
524 if (isIncomplete() || nodes.length == 0) return false;
525 return n == nodes[0] || n == nodes[nodes.length -1];
526 }
527
528 /**
529 * Replies true if the given node is an inner node of this way, false otherwise.
530 * @param n The node to test
531 * @return true if the {@code n} is an inner node, false otherwise.
532 * @since 3515
533 */
534 public boolean isInnerNode(Node n) {
535 Node[] nodes = this.nodes;
536 if (isIncomplete() || nodes.length <= 2) return false;
537 /* circular ways have only inner nodes, so return true for them! */
538 if (n == nodes[0] && n == nodes[nodes.length-1]) return true;
539 for (int i = 1; i < nodes.length - 1; ++i) {
540 if (nodes[i] == n) return true;
541 }
542 return false;
543 }
544
545 @Override
546 public OsmPrimitiveType getType() {
547 return OsmPrimitiveType.WAY;
548 }
549
550 @Override
551 public OsmPrimitiveType getDisplayType() {
552 return isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
553 }
554
555 private void checkNodes() {
556 DataSet dataSet = getDataSet();
557 if (dataSet != null) {
558 Node[] nodes = this.nodes;
559 for (Node n: nodes) {
560 if (n.getDataSet() != dataSet)
561 throw new DataIntegrityProblemException("Nodes in way must be in the same dataset",
562 tr("Nodes in way must be in the same dataset"));
563 if (n.isDeleted())
564 throw new DataIntegrityProblemException("Deleted node referenced: " + toString(),
565 "<html>" + tr("Deleted node referenced by {0}",
566 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>");
567 }
568 if (Config.getPref().getBoolean("debug.checkNullCoor", true)) {
569 for (Node n: nodes) {
570 if (n.isVisible() && !n.isIncomplete() && !n.isLatLonKnown())
571 throw new DataIntegrityProblemException("Complete visible node with null coordinates: " + toString(),
572 "<html>" + tr("Complete node {0} with null coordinates in way {1}",
573 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(n),
574 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>");
575 }
576 }
577 }
578 }
579
580 private void fireNodesChanged() {
581 checkNodes();
582 if (getDataSet() != null) {
583 getDataSet().fireWayNodesChanged(this);
584 }
585 }
586
587 @Override
588 void setDataset(DataSet dataSet) {
589 super.setDataset(dataSet);
590 checkNodes();
591 }
592
593 @Override
594 public BBox getBBox() {
595 if (getDataSet() == null)
596 return new BBox(this);
597 if (bbox == null) {
598 bbox = new BBox(this);
599 }
600 return new BBox(bbox);
601 }
602
603 @Override
604 protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
605 box.add(getBBox());
606 }
607
608 @Override
609 public void updatePosition() {
610 bbox = new BBox(this);
611 clearCachedStyle();
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 /**
629 * Replies true if all nodes of the way have known lat/lon, false otherwise.
630 * @return true if all nodes of the way have known lat/lon, false otherwise
631 * @since 13033
632 */
633 public boolean hasOnlyLocatableNodes() {
634 Node[] nodes = this.nodes;
635 for (Node node : nodes) {
636 if (!node.isLatLonKnown())
637 return false;
638 }
639 return true;
640 }
641
642 @Override
643 public boolean isUsable() {
644 return super.isUsable() && !hasIncompleteNodes();
645 }
646
647 @Override
648 public boolean isDrawable() {
649 return super.isDrawable() && hasOnlyLocatableNodes();
650 }
651
652 /**
653 * Replies the length of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
654 * @return The length of the way, in metres
655 * @since 4138
656 */
657 public double getLength() {
658 double length = 0;
659 Node lastN = null;
660 for (Node n:nodes) {
661 if (lastN != null) {
662 LatLon lastNcoor = lastN.getCoor();
663 LatLon coor = n.getCoor();
664 if (lastNcoor != null && coor != null) {
665 length += coor.greatCircleDistance(lastNcoor);
666 }
667 }
668 lastN = n;
669 }
670 return length;
671 }
672
673 /**
674 * Replies the length of the longest segment of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
675 * @return The length of the segment, in metres
676 * @since 8320
677 */
678 public double getLongestSegmentLength() {
679 double length = 0;
680 Node lastN = null;
681 for (Node n:nodes) {
682 if (lastN != null) {
683 LatLon lastNcoor = lastN.getCoor();
684 LatLon coor = n.getCoor();
685 if (lastNcoor != null && coor != null) {
686 double l = coor.greatCircleDistance(lastNcoor);
687 if (l > length) {
688 length = l;
689 }
690 }
691 }
692 lastN = n;
693 }
694 return length;
695 }
696
697 /**
698 * Tests if this way is a oneway.
699 * @return {@code 1} if the way is a oneway,
700 * {@code -1} if the way is a reversed oneway,
701 * {@code 0} otherwise.
702 * @since 5199
703 */
704 public int isOneway() {
705 String oneway = get("oneway");
706 if (oneway != null) {
707 if ("-1".equals(oneway)) {
708 return -1;
709 } else {
710 Boolean isOneway = OsmUtils.getOsmBoolean(oneway);
711 if (isOneway != null && isOneway) {
712 return 1;
713 }
714 }
715 }
716 return 0;
717 }
718
719 /**
720 * Replies the first node of this way, respecting or not its oneway state.
721 * @param respectOneway If true and if this way is a reversed oneway, replies the last node. Otherwise, replies the first node.
722 * @return the first node of this way, according to {@code respectOneway} and its oneway state.
723 * @since 5199
724 */
725 public Node firstNode(boolean respectOneway) {
726 return !respectOneway || isOneway() != -1 ? firstNode() : lastNode();
727 }
728
729 /**
730 * Replies the last node of this way, respecting or not its oneway state.
731 * @param respectOneway If true and if this way is a reversed oneway, replies the first node. Otherwise, replies the last node.
732 * @return the last node of this way, according to {@code respectOneway} and its oneway state.
733 * @since 5199
734 */
735 public Node lastNode(boolean respectOneway) {
736 return !respectOneway || isOneway() != -1 ? lastNode() : firstNode();
737 }
738
739 @Override
740 public boolean concernsArea() {
741 return hasAreaTags();
742 }
743
744 @Override
745 public boolean isOutsideDownloadArea() {
746 for (final Node n : nodes) {
747 if (n.isOutsideDownloadArea()) {
748 return true;
749 }
750 }
751 return false;
752 }
753
754 @Override
755 protected void keysChangedImpl(Map<String, String> originalKeys) {
756 super.keysChangedImpl(originalKeys);
757 clearCachedNodeStyles();
758 }
759
760 /**
761 * Clears all cached styles for all nodes of this way. This should not be called from outside.
762 * @see Node#clearCachedStyle()
763 */
764 public void clearCachedNodeStyles() {
765 for (final Node n : nodes) {
766 n.clearCachedStyle();
767 }
768 }
769
770 /**
771 * Returns angles of vertices.
772 * @return angles of the way
773 * @since 13670
774 */
775 public synchronized List<Pair<Double, Node>> getAngles() {
776 List<Pair<Double, Node>> angles = new ArrayList<>();
777
778 for (int i = 1; i < nodes.length - 1; i++) {
779 Node n0 = nodes[i - 1];
780 Node n1 = nodes[i];
781 Node n2 = nodes[i + 1];
782
783 double angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
784 n0.getEastNorth(), n1.getEastNorth(), n2.getEastNorth()));
785 angles.add(new Pair<>(angle, n1));
786 }
787
788 angles.add(new Pair<>(Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
789 nodes[nodes.length - 2].getEastNorth(),
790 nodes[0].getEastNorth(),
791 nodes[1].getEastNorth())), nodes[0]));
792
793 return angles;
794 }
795}
Note: See TracBrowser for help on using the repository browser.