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

Last change on this file was 18794, checked in by taylor.smock, 9 months ago

Fix #23085: Improve speed of selecting large amounts of objects

  • JVM CPU usage was down ~50%, EDT CPU usage was down ~25%. EDT memory usage was down
  • Avoid Stream allocations in ConflictCollection.hasConflictForMy by only looking for a conflict if conflicts exist
  • Avoid many string instantiations in DefaultNameFormatter by using cached properties. This significantly reduces memory allocations CPU usage for DefaultNameFormatter methods.
  • Avoid some Stream allocations by using standard for loops in DefaultNameFormatter
  • Use "" to get the component in PrimitiveRenderer.getListCellRendererComponent -- this reduced the memory allocations by ~50% and CPU usage by ~70% for getListCellRendererComponent by itself, and appears to have no side-effects. We should ask users on different systems with different UI systems if it works properly.
  • Significantly reduce cost of Way.hasOnlyLocatableNodes by using a standard for loop instead of a stream -- this isn't as important for this ticket, but was found while profiling. This makes that method have no effective memory allocations and reduces the CPU usage by ~90%.
  • Property svn:eol-style set to native
File size: 25.8 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;
13import java.util.stream.DoubleStream;
14import java.util.stream.IntStream;
15
16import org.openstreetmap.josm.data.coor.ILatLon;
17import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
18import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
19import org.openstreetmap.josm.spi.preferences.Config;
20import org.openstreetmap.josm.tools.CopyList;
21import org.openstreetmap.josm.tools.Geometry;
22import org.openstreetmap.josm.tools.Pair;
23import org.openstreetmap.josm.tools.Utils;
24
25/**
26 * One full way, consisting of a list of way {@link Node nodes}.
27 *
28 * @author imi
29 * @since 64
30 */
31public final class Way extends OsmPrimitive implements IWay<Node> {
32
33 static final UniqueIdGenerator idGenerator = new UniqueIdGenerator();
34 private static final Node[] EMPTY_NODES = new Node[0];
35
36 /**
37 * All way nodes in this way
38 */
39 private Node[] nodes = EMPTY_NODES;
40 private BBox bbox;
41
42 @Override
43 public List<Node> getNodes() {
44 return new CopyList<>(nodes);
45 }
46
47 @Override
48 public void setNodes(List<Node> nodes) {
49 checkDatasetNotReadOnly();
50 boolean locked = writeLock();
51 try {
52 for (Node node:this.nodes) {
53 node.removeReferrer(this);
54 node.clearCachedStyle();
55 }
56
57 if (Utils.isEmpty(nodes)) {
58 this.nodes = EMPTY_NODES;
59 } else {
60 this.nodes = nodes.toArray(EMPTY_NODES);
61 }
62 for (Node node: this.nodes) {
63 node.addReferrer(this);
64 node.clearCachedStyle();
65 }
66
67 clearCachedStyle();
68 fireNodesChanged();
69 } finally {
70 writeUnlock(locked);
71 }
72 }
73
74 /**
75 * Prevent directly following identical nodes in ways.
76 * @param nodes list of nodes
77 * @return {@code nodes} with consecutive identical nodes removed
78 */
79 private static List<Node> removeDouble(List<Node> nodes) {
80 Node last = null;
81 int count = nodes.size();
82 for (int i = 0; i < count && count > 2;) {
83 Node n = nodes.get(i);
84 if (last == n) {
85 nodes.remove(i);
86 --count;
87 } else {
88 last = n;
89 ++i;
90 }
91 }
92 return nodes;
93 }
94
95 @Override
96 public int getNodesCount() {
97 return nodes.length;
98 }
99
100 @Override
101 public Node getNode(int index) {
102 return nodes[index];
103 }
104
105 @Override
106 public long getNodeId(int idx) {
107 return nodes[idx].getUniqueId();
108 }
109
110 @Override
111 public List<Long> getNodeIds() {
112 return Arrays.stream(nodes).map(Node::getId).collect(Collectors.toList());
113 }
114
115 /**
116 * Replies true if this way contains the node <code>node</code>, false
117 * otherwise. Replies false if <code>node</code> is null.
118 *
119 * @param node the node. May be null.
120 * @return true if this way contains the node <code>node</code>, false
121 * otherwise
122 * @since 1911
123 */
124 public boolean containsNode(Node node) {
125 return node != null && Arrays.asList(nodes).contains(node);
126 }
127
128 /**
129 * Return nodes adjacent to <code>node</code>
130 *
131 * @param node the node. May be null.
132 * @return Set of nodes adjacent to <code>node</code>
133 * @since 4671
134 */
135 public Set<Node> getNeighbours(Node node) {
136 Set<Node> neigh = new HashSet<>();
137
138 if (node == null) return neigh;
139
140 for (int i = 0; i < nodes.length; i++) {
141 if (nodes[i].equals(node)) {
142 if (i > 0)
143 neigh.add(nodes[i-1]);
144 if (i < nodes.length-1)
145 neigh.add(nodes[i+1]);
146 }
147 }
148 return neigh;
149 }
150
151 /**
152 * Replies the ordered {@link List} of chunks of this way. Each chunk is replied as a {@link Pair} of {@link Node nodes}.
153 * @param sort If true, the nodes of each pair are sorted as defined by {@link Pair#sort}.
154 * If false, Pair.a and Pair.b are in the way order
155 * (i.e for a given Pair(n), Pair(n-1).b == Pair(n).a, Pair(n).b == Pair(n+1).a, etc.)
156 * @return The ordered list of chunks of this way.
157 * @since 3348
158 */
159 public List<Pair<Node, Node>> getNodePairs(boolean sort) {
160 // For a way of size n, there are n - 1 pairs (a -> b, b -> c, c -> d, etc., 4 nodes -> 3 pairs)
161 List<Pair<Node, Node>> chunkSet = new ArrayList<>(Math.max(0, this.getNodesCount() - 1));
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 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 * This adds links from all way nodes to the clone. See #19885 for possible memory leaks.
202 * @param original The original {@code Way} to be identically cloned. Must not be null
203 * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
204 * If {@code false}, does nothing
205 * @param copyNodes whether to copy nodes too
206 * @since 16212
207 */
208 public Way(Way original, boolean clearMetadata, boolean copyNodes) {
209 super(original.getUniqueId(), true);
210 cloneFrom(original, copyNodes);
211 if (clearMetadata) {
212 clearOsmMetadata();
213 }
214 }
215
216 /**
217 * Constructs a new {@code Way} from an existing {@code Way}.
218 * This adds links from all way nodes to the clone. See #19885 for possible memory leaks.
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 this(original, clearMetadata, true);
226 }
227
228 /**
229 * Constructs a new {@code Way} from an existing {@code Way} (including its id).
230 * This adds links from all way nodes to the clone. See #19885 for possible memory leaks.
231 * @param original The original {@code Way} to be identically cloned. Must not be null
232 * @since 86
233 */
234 public Way(Way original) {
235 this(original, false);
236 }
237
238 /**
239 * Constructs a new {@code Way} for the given id. If the id &gt; 0, the way is marked
240 * as incomplete. If id == 0 then way is marked as new
241 *
242 * @param id the id. &gt;= 0 required
243 * @throws IllegalArgumentException if id &lt; 0
244 * @since 343
245 */
246 public Way(long id) {
247 super(id, false);
248 }
249
250 /**
251 * Constructs a new {@code Way} with given id and version.
252 * @param id the id. &gt;= 0 required
253 * @param version the version
254 * @throws IllegalArgumentException if id &lt; 0
255 * @since 2620
256 */
257 public Way(long id, int version) {
258 super(id, version, false);
259 }
260
261 @Override
262 public void load(PrimitiveData data) {
263 if (!(data instanceof WayData))
264 throw new IllegalArgumentException("Not a way data: " + data);
265 boolean locked = writeLock();
266 try {
267 super.load(data);
268
269 List<Long> nodeIds = ((WayData) data).getNodeIds();
270
271 if (!nodeIds.isEmpty() && getDataSet() == null) {
272 throw new AssertionError("Data consistency problem - way without dataset detected");
273 }
274
275 List<Node> newNodes = new ArrayList<>(nodeIds.size());
276 for (Long nodeId : nodeIds) {
277 Node node = (Node) getDataSet().getPrimitiveById(nodeId, OsmPrimitiveType.NODE);
278 if (node != null) {
279 newNodes.add(node);
280 } else {
281 throw new AssertionError("Data consistency problem - way with missing node detected");
282 }
283 }
284 setNodes(newNodes);
285 } finally {
286 writeUnlock(locked);
287 }
288 }
289
290 @Override
291 public WayData save() {
292 WayData data = new WayData();
293 saveCommonAttributes(data);
294 for (Node node:nodes) {
295 data.getNodeIds().add(node.getUniqueId());
296 }
297 return data;
298 }
299
300 @Override
301 public void cloneFrom(OsmPrimitive osm, boolean copyNodes) {
302 if (!(osm instanceof Way))
303 throw new IllegalArgumentException("Not a way: " + osm);
304 boolean locked = writeLock();
305 try {
306 super.cloneFrom(osm, copyNodes);
307 if (copyNodes) {
308 setNodes(((Way) osm).getNodes());
309 }
310 } finally {
311 writeUnlock(locked);
312 }
313 }
314
315 @Override
316 public String toString() {
317 String nodesDesc = isIncomplete() ? "(incomplete)" : ("nodes=" + Arrays.toString(nodes));
318 return "{Way id=" + getUniqueId() + " version=" + getVersion()+ ' ' + getFlagsAsString() + ' ' + nodesDesc + '}';
319 }
320
321 @Override
322 public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
323 if (!(other instanceof Way))
324 return false;
325 Way w = (Way) other;
326 if (getNodesCount() != w.getNodesCount()) return false;
327 if (!super.hasEqualSemanticAttributes(other, testInterestingTagsOnly))
328 return false;
329 return IntStream.range(0, getNodesCount())
330 .allMatch(i -> getNode(i).hasEqualSemanticAttributes(w.getNode(i)));
331 }
332
333 /**
334 * Removes the given {@link Node} from this way. Ignored, if n is null.
335 * @param n The node to remove. Ignored, if null
336 * @since 1463
337 */
338 public void removeNode(Node n) {
339 checkDatasetNotReadOnly();
340 if (n == null || isIncomplete()) return;
341 boolean locked = writeLock();
342 try {
343 boolean closed = lastNode() == n && firstNode() == n;
344 int i;
345 List<Node> copy = getNodes();
346 while ((i = copy.indexOf(n)) >= 0) {
347 copy.remove(i);
348 }
349 i = copy.size();
350 if (closed && i > 2) {
351 copy.add(copy.get(0));
352 } else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) {
353 copy.remove(i-1);
354 }
355 setNodes(removeDouble(copy));
356 n.clearCachedStyle();
357 } finally {
358 writeUnlock(locked);
359 }
360 }
361
362 /**
363 * Removes the given set of {@link Node nodes} from this way. Ignored, if selection is null.
364 * @param selection The selection of nodes to remove. Ignored, if null
365 * @since 5408
366 */
367 public void removeNodes(Set<? extends Node> selection) {
368 checkDatasetNotReadOnly();
369 if (selection == null || isIncomplete()) return;
370 boolean locked = writeLock();
371 try {
372 setNodes(calculateRemoveNodes(selection));
373 for (Node n : selection) {
374 n.clearCachedStyle();
375 }
376 } finally {
377 writeUnlock(locked);
378 }
379 }
380
381 /**
382 * Calculate the remaining nodes after a removal of the given set of {@link Node nodes} from this way.
383 * @param selection The selection of nodes to remove. Ignored, if null
384 * @return result of the removal, can be empty
385 * @since 17102
386 */
387 public List<Node> calculateRemoveNodes(Set<? extends Node> selection) {
388 if (selection == null || isIncomplete())
389 return getNodes();
390 boolean closed = isClosed() && selection.contains(lastNode());
391 List<Node> copy = Arrays.stream(nodes)
392 .filter(n -> !selection.contains(n))
393 .collect(Collectors.toList());
394
395 int i = copy.size();
396 if (closed && i > 2) {
397 copy.add(copy.get(0));
398 } else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) {
399 copy.remove(i-1);
400 }
401 return removeDouble(copy);
402 }
403
404 /**
405 * Adds a node to the end of the list of nodes. Ignored, if n is null.
406 *
407 * @param n the node. Ignored, if null
408 * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
409 * to an incomplete way
410 * @since 1313
411 */
412 public void addNode(Node n) {
413 checkDatasetNotReadOnly();
414 if (n == null) return;
415
416 boolean locked = writeLock();
417 try {
418 if (isIncomplete())
419 throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
420 clearCachedStyle();
421 n.addReferrer(this);
422 nodes = Utils.addInArrayCopy(nodes, n);
423 n.clearCachedStyle();
424 fireNodesChanged();
425 } finally {
426 writeUnlock(locked);
427 }
428 }
429
430 /**
431 * Adds a node at position offs.
432 *
433 * @param offs the offset
434 * @param n the node. Ignored, if null.
435 * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
436 * to an incomplete way
437 * @throws IndexOutOfBoundsException if offs is out of bounds
438 * @since 1313
439 */
440 public void addNode(int offs, Node n) {
441 checkDatasetNotReadOnly();
442 if (n == null) return;
443
444 boolean locked = writeLock();
445 try {
446 if (isIncomplete())
447 throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
448
449 clearCachedStyle();
450 n.addReferrer(this);
451 Node[] newNodes = new Node[nodes.length + 1];
452 System.arraycopy(nodes, 0, newNodes, 0, offs);
453 System.arraycopy(nodes, offs, newNodes, offs + 1, nodes.length - offs);
454 newNodes[offs] = n;
455 nodes = newNodes;
456 n.clearCachedStyle();
457 fireNodesChanged();
458 } finally {
459 writeUnlock(locked);
460 }
461 }
462
463 @Override
464 public void setDeleted(boolean deleted) {
465 boolean locked = writeLock();
466 try {
467 for (Node n:nodes) {
468 if (deleted) {
469 n.removeReferrer(this);
470 } else {
471 n.addReferrer(this);
472 }
473 n.clearCachedStyle();
474 }
475 fireNodesChanged();
476 super.setDeleted(deleted);
477 } finally {
478 writeUnlock(locked);
479 }
480 }
481
482 @Override
483 public boolean isClosed() {
484 if (isIncomplete()) return false;
485
486 return nodes.length >= 3 && nodes[nodes.length-1] == nodes[0];
487 }
488
489 /**
490 * Determines if this way denotes an area (closed way with at least three distinct nodes).
491 * @return {@code true} if this way is closed and contains at least three distinct nodes
492 * @see #isClosed
493 * @since 5490
494 */
495 public boolean isArea() {
496 if (this.nodes.length >= 4 && isClosed()) {
497 Node distinctNode = null;
498 for (int i = 1; i < nodes.length-1; i++) {
499 if (distinctNode == null && nodes[i] != nodes[0]) {
500 distinctNode = nodes[i];
501 } else if (distinctNode != null && nodes[i] != nodes[0] && nodes[i] != distinctNode) {
502 return true;
503 }
504 }
505 }
506 return false;
507 }
508
509 @Override
510 public Node lastNode() {
511 if (isIncomplete() || nodes.length == 0) return null;
512 return nodes[nodes.length-1];
513 }
514
515 @Override
516 public Node firstNode() {
517 if (isIncomplete() || nodes.length == 0) return null;
518 return nodes[0];
519 }
520
521 @Override
522 public boolean isFirstLastNode(INode n) {
523 if (isIncomplete() || nodes.length == 0) return false;
524 return n == nodes[0] || n == nodes[nodes.length -1];
525 }
526
527 @Override
528 public boolean isInnerNode(INode n) {
529 if (isIncomplete() || nodes.length <= 2) return false;
530 /* circular ways have only inner nodes, so return true for them! */
531 if (n == nodes[0] && n == nodes[nodes.length-1]) return true;
532 return IntStream.range(1, nodes.length - 1)
533 .anyMatch(i -> nodes[i] == n);
534 }
535
536 @Override
537 public OsmPrimitiveType getType() {
538 return OsmPrimitiveType.WAY;
539 }
540
541 @Override
542 public OsmPrimitiveType getDisplayType() {
543 return isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
544 }
545
546 private void checkNodes() {
547 DataSet dataSet = getDataSet();
548 if (dataSet != null) {
549 for (Node n: nodes) {
550 if (n.getDataSet() != dataSet)
551 throw new DataIntegrityProblemException("Nodes in way must be in the same dataset",
552 tr("Nodes in way must be in the same dataset"));
553 if (n.isDeleted())
554 throw new DataIntegrityProblemException("Deleted node referenced: " + toString(),
555 "<html>" + tr("Deleted node referenced by {0}",
556 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>",
557 this, n);
558 }
559 if (Config.getPref().getBoolean("debug.checkNullCoor", true)) {
560 for (Node n: nodes) {
561 if (n.isVisible() && !n.isIncomplete() && !n.isLatLonKnown())
562 throw new DataIntegrityProblemException("Complete visible node with null coordinates: " + toString(),
563 "<html>" + tr("Complete node {0} with null coordinates in way {1}",
564 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(n),
565 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>",
566 this, n);
567 }
568 }
569 }
570 }
571
572 private void fireNodesChanged() {
573 checkNodes();
574 if (getDataSet() != null) {
575 getDataSet().fireWayNodesChanged(this);
576 }
577 }
578
579 @Override
580 void setDataset(DataSet dataSet) {
581 super.setDataset(dataSet);
582 checkNodes();
583 }
584
585 @Override
586 public BBox getBBox() {
587 if (getDataSet() == null)
588 return new BBox(this);
589 if (bbox == null) {
590 setBBox(new BBox(this));
591 }
592 return bbox;
593 }
594
595 @Override
596 protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
597 box.add(getBBox());
598 }
599
600 private void setBBox(BBox bbox) {
601 this.bbox = bbox == null ? null : bbox.toImmutable();
602 }
603
604 @Override
605 public void updatePosition() {
606 setBBox(new BBox(this));
607 clearCachedStyle();
608 }
609
610 @Override
611 public boolean hasIncompleteNodes() {
612 /*
613 * Ideally, we would store this as a flag, but a node may become
614 * incomplete under some circumstances without being able to notify the
615 * way to recalculate the flag.
616 *
617 * When profiling #20716 on Mesa County, CO (overpass download), the
618 * Arrays.stream method was fairly expensive. When switching to the for
619 * loop, the CPU samples for hasIncompleteNodes went from ~150k samples
620 * to ~8.5k samples (94% improvement) and the memory allocations for
621 * hasIncompleteNodes went from ~15.6 GB to 0.
622 */
623 for (Node node : nodes) {
624 if (node.isIncomplete()) {
625 return true;
626 }
627 }
628 return false;
629 }
630
631 /**
632 * Replies true if all nodes of the way have known lat/lon, false otherwise.
633 * @return true if all nodes of the way have known lat/lon, false otherwise
634 * @since 13033
635 */
636 public boolean hasOnlyLocatableNodes() {
637 // This is used in various places, some of which are on the UI thread.
638 // If this is called many times, the memory allocations can become prohibitive, if
639 // we use Java streams.
640 // This can be easily tested by loading a large amount of ways into JOSM, and then
641 // selecting everything.
642 for (Node node : nodes) {
643 if (!node.isLatLonKnown()) {
644 return false;
645 }
646 }
647 return true;
648 }
649
650 @Override
651 public boolean isUsable() {
652 return super.isUsable() && !hasIncompleteNodes();
653 }
654
655 @Override
656 public boolean isDrawable() {
657 return super.isDrawable() && hasOnlyLocatableNodes();
658 }
659
660 /**
661 * Replies the length of the way, in metres, as computed by {@link ILatLon#greatCircleDistance}.
662 * @return The length of the way, in metres
663 * @since 4138
664 */
665 public double getLength() {
666 double length = 0;
667 Node lastN = null;
668 for (Node n:nodes) {
669 if (lastN != null && lastN.isLatLonKnown() && n.isLatLonKnown()) {
670 length += n.greatCircleDistance(lastN);
671 }
672 lastN = n;
673 }
674 return length;
675 }
676
677 /**
678 * Replies the segment lengths of the way as computed by {@link ILatLon#greatCircleDistance}.
679 *
680 * @return The segment lengths of a way in metres, following way direction
681 * @since 18553
682 */
683 public double[] getSegmentLengths() {
684 return this.segmentLengths().toArray();
685 }
686
687 /**
688 * Replies the length of the longest segment of the way, in metres, as computed by {@link ILatLon#greatCircleDistance}.
689 * @return The length of the segment, in metres
690 * @since 8320
691 */
692 public double getLongestSegmentLength() {
693 return this.segmentLengths().max().orElse(0);
694 }
695
696 /**
697 * Get the segment lengths as a stream
698 * @return The stream of segment lengths (ordered)
699 */
700 private DoubleStream segmentLengths() {
701 DoubleStream.Builder builder = DoubleStream.builder();
702 Node lastN = null;
703 for (Node n : nodes) {
704 if (lastN != null && n.isLatLonKnown() && lastN.isLatLonKnown()) {
705 double distance = n.greatCircleDistance(lastN);
706 builder.accept(distance);
707 }
708 lastN = n;
709 }
710 return builder.build();
711 }
712
713 /**
714 * Tests if this way is a oneway.
715 * @return {@code 1} if the way is a oneway,
716 * {@code -1} if the way is a reversed oneway,
717 * {@code 0} otherwise.
718 * @since 5199
719 */
720 public int isOneway() {
721 String oneway = get("oneway");
722 if (oneway != null) {
723 if ("-1".equals(oneway)) {
724 return -1;
725 } else {
726 Boolean isOneway = OsmUtils.getOsmBoolean(oneway);
727 if (isOneway != null && isOneway) {
728 return 1;
729 }
730 }
731 }
732 return 0;
733 }
734
735 /**
736 * Replies the first node of this way, respecting or not its oneway state.
737 * @param respectOneway If true and if this way is a reversed oneway, replies the last node. Otherwise, replies the first node.
738 * @return the first node of this way, according to {@code respectOneway} and its oneway state.
739 * @since 5199
740 */
741 public Node firstNode(boolean respectOneway) {
742 return !respectOneway || isOneway() != -1 ? firstNode() : lastNode();
743 }
744
745 /**
746 * Replies the last node of this way, respecting or not its oneway state.
747 * @param respectOneway If true and if this way is a reversed oneway, replies the first node. Otherwise, replies the last node.
748 * @return the last node of this way, according to {@code respectOneway} and its oneway state.
749 * @since 5199
750 */
751 public Node lastNode(boolean respectOneway) {
752 return !respectOneway || isOneway() != -1 ? lastNode() : firstNode();
753 }
754
755 @Override
756 public boolean concernsArea() {
757 return hasAreaTags();
758 }
759
760 @Override
761 public boolean isOutsideDownloadArea() {
762 return Arrays.stream(nodes).anyMatch(Node::isOutsideDownloadArea);
763 }
764
765 @Override
766 protected void keysChangedImpl(Map<String, String> originalKeys) {
767 super.keysChangedImpl(originalKeys);
768 clearCachedNodeStyles();
769 }
770
771 /**
772 * Clears all cached styles for all nodes of this way. This should not be called from outside.
773 * @see Node#clearCachedStyle()
774 */
775 public void clearCachedNodeStyles() {
776 for (final Node n : nodes) {
777 n.clearCachedStyle();
778 }
779 }
780
781 /**
782 * Returns angles of vertices.
783 * @return angles of the way
784 * @since 13670
785 */
786 public synchronized List<Pair<Double, Node>> getAngles() {
787 List<Pair<Double, Node>> angles = new ArrayList<>();
788
789 for (int i = 1; i < nodes.length - 1; i++) {
790 Node n0 = nodes[i - 1];
791 Node n1 = nodes[i];
792 Node n2 = nodes[i + 1];
793
794 double angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
795 n0.getEastNorth(), n1.getEastNorth(), n2.getEastNorth()));
796 angles.add(new Pair<>(angle, n1));
797 }
798
799 angles.add(new Pair<>(Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
800 nodes[nodes.length - 2].getEastNorth(),
801 nodes[0].getEastNorth(),
802 nodes[1].getEastNorth())), nodes[0]));
803
804 return angles;
805 }
806
807 @Override
808 public UniqueIdGenerator getIdGenerator() {
809 return idGenerator;
810 }
811}
Note: See TracBrowser for help on using the repository browser.