source: josm/trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/relations/Multipolygon.java@ 16445

Last change on this file since 16445 was 16445, checked in by simon04, 4 years ago

see #19251 - Java 8: use Stream

  • Property svn:eol-style set to native
File size: 27.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm.visitor.paint.relations;
3
4import java.awt.geom.Path2D;
5import java.awt.geom.PathIterator;
6import java.awt.geom.Rectangle2D;
7import java.util.ArrayList;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.HashSet;
11import java.util.Iterator;
12import java.util.List;
13import java.util.Optional;
14import java.util.Set;
15
16import org.openstreetmap.josm.data.coor.EastNorth;
17import org.openstreetmap.josm.data.osm.DataSet;
18import org.openstreetmap.josm.data.osm.Node;
19import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
20import org.openstreetmap.josm.data.osm.Relation;
21import org.openstreetmap.josm.data.osm.RelationMember;
22import org.openstreetmap.josm.data.osm.Way;
23import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
24import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
25import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
26import org.openstreetmap.josm.data.projection.Projection;
27import org.openstreetmap.josm.data.projection.ProjectionRegistry;
28import org.openstreetmap.josm.spi.preferences.Config;
29import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
30import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
31import org.openstreetmap.josm.tools.Geometry;
32import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter;
33import org.openstreetmap.josm.tools.Logging;
34
35/**
36 * Multipolygon data used to represent complex areas, see <a href="https://wiki.openstreetmap.org/wiki/Relation:multipolygon">wiki</a>.
37 * @since 2788
38 */
39public class Multipolygon {
40
41 /** preference key for a collection of roles which indicate that the respective member belongs to an
42 * <em>outer</em> polygon. Default is <code>outer</code>.
43 */
44 public static final String PREF_KEY_OUTER_ROLES = "mappaint.multipolygon.outer.roles";
45
46 /** preference key for collection of role prefixes which indicate that the respective
47 * member belongs to an <em>outer</em> polygon. Default is empty.
48 */
49 public static final String PREF_KEY_OUTER_ROLE_PREFIXES = "mappaint.multipolygon.outer.role-prefixes";
50
51 /** preference key for a collection of roles which indicate that the respective member belongs to an
52 * <em>inner</em> polygon. Default is <code>inner</code>.
53 */
54 public static final String PREF_KEY_INNER_ROLES = "mappaint.multipolygon.inner.roles";
55
56 /** preference key for collection of role prefixes which indicate that the respective
57 * member belongs to an <em>inner</em> polygon. Default is empty.
58 */
59 public static final String PREF_KEY_INNER_ROLE_PREFIXES = "mappaint.multipolygon.inner.role-prefixes";
60
61 /**
62 * <p>Kind of strategy object which is responsible for deciding whether a given
63 * member role indicates that the member belongs to an <em>outer</em> or an
64 * <em>inner</em> polygon.</p>
65 *
66 * <p>The decision is taken based on preference settings, see the four preference keys
67 * above.</p>
68 */
69 private static class MultipolygonRoleMatcher implements PreferenceChangedListener {
70 private final List<String> outerExactRoles = new ArrayList<>();
71 private final List<String> outerRolePrefixes = new ArrayList<>();
72 private final List<String> innerExactRoles = new ArrayList<>();
73 private final List<String> innerRolePrefixes = new ArrayList<>();
74
75 private void initDefaults() {
76 outerExactRoles.clear();
77 outerRolePrefixes.clear();
78 innerExactRoles.clear();
79 innerRolePrefixes.clear();
80 outerExactRoles.add("outer");
81 innerExactRoles.add("inner");
82 }
83
84 private static void setNormalized(Collection<String> literals, List<String> target) {
85 target.clear();
86 for (String l: literals) {
87 if (l == null) {
88 continue;
89 }
90 l = l.trim();
91 if (!target.contains(l)) {
92 target.add(l);
93 }
94 }
95 }
96
97 private void initFromPreferences() {
98 initDefaults();
99 if (Config.getPref() == null) return;
100 Collection<String> literals;
101 literals = Config.getPref().getList(PREF_KEY_OUTER_ROLES);
102 if (literals != null && !literals.isEmpty()) {
103 setNormalized(literals, outerExactRoles);
104 }
105 literals = Config.getPref().getList(PREF_KEY_OUTER_ROLE_PREFIXES);
106 if (literals != null && !literals.isEmpty()) {
107 setNormalized(literals, outerRolePrefixes);
108 }
109 literals = Config.getPref().getList(PREF_KEY_INNER_ROLES);
110 if (literals != null && !literals.isEmpty()) {
111 setNormalized(literals, innerExactRoles);
112 }
113 literals = Config.getPref().getList(PREF_KEY_INNER_ROLE_PREFIXES);
114 if (literals != null && !literals.isEmpty()) {
115 setNormalized(literals, innerRolePrefixes);
116 }
117 }
118
119 @Override
120 public void preferenceChanged(PreferenceChangeEvent evt) {
121 if (PREF_KEY_INNER_ROLE_PREFIXES.equals(evt.getKey()) ||
122 PREF_KEY_INNER_ROLES.equals(evt.getKey()) ||
123 PREF_KEY_OUTER_ROLE_PREFIXES.equals(evt.getKey()) ||
124 PREF_KEY_OUTER_ROLES.equals(evt.getKey())) {
125 initFromPreferences();
126 }
127 }
128
129 boolean isOuterRole(String role) {
130 if (role == null) return false;
131 return outerExactRoles.stream().anyMatch(role::equals) || outerRolePrefixes.stream().anyMatch(role::startsWith);
132 }
133
134 boolean isInnerRole(String role) {
135 if (role == null) return false;
136 return innerExactRoles.stream().anyMatch(role::equals) || innerRolePrefixes.stream().anyMatch(role::startsWith);
137 }
138 }
139
140 /*
141 * Init a private global matcher object which will listen to preference changes.
142 */
143 private static MultipolygonRoleMatcher roleMatcher;
144
145 private static synchronized MultipolygonRoleMatcher getMultipolygonRoleMatcher() {
146 if (roleMatcher == null) {
147 roleMatcher = new MultipolygonRoleMatcher();
148 if (Config.getPref() != null) {
149 roleMatcher.initFromPreferences();
150 Config.getPref().addPreferenceChangeListener(roleMatcher);
151 }
152 }
153 return roleMatcher;
154 }
155
156 /**
157 * Class representing a string of ways.
158 *
159 * The last node of one way is the first way of the next one.
160 * The string may or may not be closed.
161 */
162 public static class JoinedWay {
163 protected final List<Node> nodes;
164 protected final Collection<Long> wayIds;
165 protected boolean selected;
166
167 /**
168 * Constructs a new {@code JoinedWay}.
169 * @param nodes list of nodes - must not be null
170 * @param wayIds list of way IDs - must not be null
171 * @param selected whether joined way is selected or not
172 */
173 public JoinedWay(List<Node> nodes, Collection<Long> wayIds, boolean selected) {
174 this.nodes = new ArrayList<>(nodes);
175 // see #17819
176 final int size = wayIds.size();
177 if (size == 1) {
178 this.wayIds = Collections.singleton(wayIds.iterator().next());
179 } else {
180 this.wayIds = size <= 10 ? new ArrayList<>(wayIds) : new HashSet<>(wayIds);
181 }
182 this.selected = selected;
183 }
184
185 /**
186 * Replies the list of nodes.
187 * @return the list of nodes
188 */
189 public List<Node> getNodes() {
190 return Collections.unmodifiableList(nodes);
191 }
192
193 /**
194 * Replies the list of way IDs.
195 * @return the list of way IDs
196 */
197 public Collection<Long> getWayIds() {
198 return Collections.unmodifiableCollection(wayIds);
199 }
200
201 /**
202 * Determines if this is selected.
203 * @return {@code true} if this is selected
204 */
205 public final boolean isSelected() {
206 return selected;
207 }
208
209 /**
210 * Sets whether this is selected
211 * @param selected {@code true} if this is selected
212 * @since 10312
213 */
214 public final void setSelected(boolean selected) {
215 this.selected = selected;
216 }
217
218 /**
219 * Determines if this joined way is closed.
220 * @return {@code true} if this joined way is closed
221 */
222 public boolean isClosed() {
223 return nodes.isEmpty() || getLastNode().equals(getFirstNode());
224 }
225
226 /**
227 * Returns the first node.
228 * @return the first node
229 * @since 10312
230 */
231 public Node getFirstNode() {
232 return nodes.get(0);
233 }
234
235 /**
236 * Returns the last node.
237 * @return the last node
238 * @since 10312
239 */
240 public Node getLastNode() {
241 return nodes.get(nodes.size() - 1);
242 }
243 }
244
245 /**
246 * The polygon data for a multipolygon part.
247 * It contains the outline of this polygon in east/north space.
248 */
249 public static class PolyData extends JoinedWay {
250 /**
251 * The intersection type used for {@link PolyData#contains(java.awt.geom.Path2D.Double)}
252 */
253 public enum Intersection {
254 /**
255 * The polygon is completely inside this PolyData
256 */
257 INSIDE,
258 /**
259 * The polygon is completely outside of this PolyData
260 */
261 OUTSIDE,
262 /**
263 * The polygon is partially inside and outside of this PolyData
264 */
265 CROSSING
266 }
267
268 private final Path2D.Double poly;
269 private Rectangle2D bounds;
270 private final List<PolyData> inners;
271
272 /**
273 * Constructs a new {@code PolyData} from a closed way.
274 * @param closedWay closed way
275 */
276 public PolyData(Way closedWay) {
277 this(closedWay.getNodes(), closedWay.isSelected(), Collections.singleton(closedWay.getUniqueId()));
278 }
279
280 /**
281 * Constructs a new {@code PolyData} from a {@link JoinedWay}.
282 * @param joinedWay joined way
283 */
284 public PolyData(JoinedWay joinedWay) {
285 this(joinedWay.nodes, joinedWay.selected, joinedWay.wayIds);
286 }
287
288 private PolyData(List<Node> nodes, boolean selected, Collection<Long> wayIds) {
289 super(nodes, wayIds, selected);
290 this.inners = new ArrayList<>();
291 this.poly = new Path2D.Double();
292 this.poly.setWindingRule(Path2D.WIND_EVEN_ODD);
293 buildPoly();
294 }
295
296 /**
297 * Constructs a new {@code PolyData} from an existing {@code PolyData}.
298 * @param copy existing instance
299 */
300 public PolyData(PolyData copy) {
301 super(copy.nodes, copy.wayIds, copy.selected);
302 this.poly = (Path2D.Double) copy.poly.clone();
303 this.inners = new ArrayList<>(copy.inners);
304 }
305
306 private void buildPoly() {
307 boolean initial = true;
308 for (Node n : nodes) {
309 EastNorth p = n.getEastNorth();
310 if (p != null) {
311 if (initial) {
312 poly.moveTo(p.getX(), p.getY());
313 initial = false;
314 } else {
315 poly.lineTo(p.getX(), p.getY());
316 }
317 }
318 }
319 if (nodes.size() >= 3 && nodes.get(0) == nodes.get(nodes.size() - 1)) {
320 poly.closePath();
321 }
322 for (PolyData inner : inners) {
323 appendInner(inner.poly);
324 }
325 }
326
327 /**
328 * Checks if this multipolygon contains or crosses an other polygon. This is a quick+lazy test which assumes
329 * that a polygon is inside when all points are inside. It will fail when the polygon encloses a hole or crosses
330 * the edges of poly so that both end points are inside poly (think of a square overlapping a U-shape).
331 * @param p The path to check. Needs to be in east/north space.
332 * @return a {@link Intersection} constant
333 */
334 public Intersection contains(Path2D.Double p) {
335 int contains = 0;
336 int total = 0;
337 double[] coords = new double[6];
338 for (PathIterator it = p.getPathIterator(null); !it.isDone(); it.next()) {
339 switch (it.currentSegment(coords)) {
340 case PathIterator.SEG_MOVETO:
341 case PathIterator.SEG_LINETO:
342 if (poly.contains(coords[0], coords[1])) {
343 contains++;
344 }
345 total++;
346 break;
347 default: // Do nothing
348 }
349 }
350 if (contains == total) return Intersection.INSIDE;
351 if (contains == 0) return Intersection.OUTSIDE;
352 return Intersection.CROSSING;
353 }
354
355 /**
356 * Adds an inner polygon
357 * @param inner The polygon to add as inner polygon.
358 */
359 public void addInner(PolyData inner) {
360 inners.add(inner);
361 appendInner(inner.poly);
362 }
363
364 private void appendInner(Path2D.Double inner) {
365 poly.append(inner.getPathIterator(null), false);
366 }
367
368 /**
369 * Gets the polygon outline and interior as java path
370 * @return The path in east/north space.
371 */
372 public Path2D.Double get() {
373 return poly;
374 }
375
376 /**
377 * Gets the bounds as {@link Rectangle2D} in east/north space.
378 * @return The bounds
379 */
380 public Rectangle2D getBounds() {
381 if (bounds == null) {
382 bounds = poly.getBounds2D();
383 }
384 return bounds;
385 }
386
387 /**
388 * Gets a list of all inner polygons.
389 * @return The inner polygons.
390 */
391 public List<PolyData> getInners() {
392 return Collections.unmodifiableList(inners);
393 }
394
395 private void resetNodes(DataSet dataSet) {
396 if (!nodes.isEmpty()) {
397 DataSet ds = dataSet;
398 // Find DataSet (can be null for several nodes when undoing nodes creation, see #7162)
399 for (Iterator<Node> it = nodes.iterator(); it.hasNext() && ds == null;) {
400 ds = it.next().getDataSet();
401 }
402 nodes.clear();
403 if (ds == null) {
404 // DataSet still not found. This should not happen, but a warning does no harm
405 Logging.warn("DataSet not found while resetting nodes in Multipolygon. " +
406 "This should not happen, you may report it to JOSM developers.");
407 } else if (wayIds.size() == 1) {
408 Way w = (Way) ds.getPrimitiveById(wayIds.iterator().next(), OsmPrimitiveType.WAY);
409 nodes.addAll(w.getNodes());
410 } else if (!wayIds.isEmpty()) {
411 List<Way> waysToJoin = new ArrayList<>();
412 for (Long wayId : wayIds) {
413 Way w = (Way) ds.getPrimitiveById(wayId, OsmPrimitiveType.WAY);
414 if (w != null && !w.isEmpty()) { // fix #7173 (empty ways on purge)
415 waysToJoin.add(w);
416 }
417 }
418 if (!waysToJoin.isEmpty()) {
419 nodes.addAll(joinWays(waysToJoin).iterator().next().getNodes());
420 }
421 }
422 resetPoly();
423 }
424 }
425
426 private void resetPoly() {
427 poly.reset();
428 buildPoly();
429 bounds = null;
430 }
431
432 /**
433 * Check if this polygon was changed by a node move
434 * @param event The node move event
435 */
436 public void nodeMoved(NodeMovedEvent event) {
437 final Node n = event.getNode();
438 boolean innerChanged = false;
439 for (PolyData inner : inners) {
440 if (inner.nodes.contains(n)) {
441 inner.resetPoly();
442 innerChanged = true;
443 }
444 }
445 if (nodes.contains(n) || innerChanged) {
446 resetPoly();
447 }
448 }
449
450 /**
451 * Check if this polygon was affected by a way change
452 * @param event The way event
453 */
454 public void wayNodesChanged(WayNodesChangedEvent event) {
455 final Long wayId = event.getChangedWay().getUniqueId();
456 boolean innerChanged = false;
457 for (PolyData inner : inners) {
458 if (inner.wayIds.contains(wayId)) {
459 inner.resetNodes(event.getDataset());
460 innerChanged = true;
461 }
462 }
463 if (wayIds.contains(wayId) || innerChanged) {
464 resetNodes(event.getDataset());
465 }
466 }
467
468 @Override
469 public boolean isClosed() {
470 if (nodes.size() < 3 || !getFirstNode().equals(getLastNode()))
471 return false;
472 return inners.stream().allMatch(PolyData::isClosed);
473 }
474
475 /**
476 * Calculate area and perimeter length in the given projection.
477 *
478 * @param projection the projection to use for the calculation, {@code null} defaults to {@link ProjectionRegistry#getProjection()}
479 * @return area and perimeter
480 */
481 public AreaAndPerimeter getAreaAndPerimeter(Projection projection) {
482 AreaAndPerimeter ap = Geometry.getAreaAndPerimeter(nodes, projection);
483 double area = ap.getArea();
484 double perimeter = ap.getPerimeter();
485 for (PolyData inner : inners) {
486 AreaAndPerimeter apInner = inner.getAreaAndPerimeter(projection);
487 area -= apInner.getArea();
488 perimeter += apInner.getPerimeter();
489 }
490 return new AreaAndPerimeter(area, perimeter);
491 }
492 }
493
494 private final List<Way> innerWays = new ArrayList<>();
495 private final List<Way> outerWays = new ArrayList<>();
496 private final List<PolyData> combinedPolygons = new ArrayList<>();
497 private final List<Node> openEnds = new ArrayList<>();
498
499 private boolean incomplete;
500
501 /**
502 * Constructs a new {@code Multipolygon} from a relation.
503 * @param r relation
504 */
505 public Multipolygon(Relation r) {
506 load(r);
507 }
508
509 private void load(Relation r) {
510 MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher();
511
512 // Fill inner and outer list with valid ways
513 for (RelationMember m : r.getMembers()) {
514 if (m.getMember().isIncomplete()) {
515 this.incomplete = true;
516 } else if (m.getMember().isDrawable() && m.isWay()) {
517 Way w = m.getWay();
518
519 if (w.getNodesCount() < 2) {
520 continue;
521 }
522
523 if (matcher.isInnerRole(m.getRole())) {
524 innerWays.add(w);
525 } else if (!m.hasRole() || matcher.isOuterRole(m.getRole())) {
526 outerWays.add(w);
527 } // Remaining roles ignored
528 } // Non ways ignored
529 }
530
531 final List<PolyData> innerPolygons = new ArrayList<>();
532 final List<PolyData> outerPolygons = new ArrayList<>();
533 createPolygons(innerWays, innerPolygons);
534 createPolygons(outerWays, outerPolygons);
535 if (!outerPolygons.isEmpty()) {
536 addInnerToOuters(innerPolygons, outerPolygons);
537 }
538 }
539
540 /**
541 * Determines if this multipolygon is incomplete.
542 * @return {@code true} if this multipolygon is incomplete
543 */
544 public final boolean isIncomplete() {
545 return incomplete;
546 }
547
548 private void createPolygons(List<Way> ways, List<PolyData> result) {
549 List<Way> waysToJoin = new ArrayList<>();
550 for (Way way: ways) {
551 if (way.isClosed()) {
552 result.add(new PolyData(way));
553 } else {
554 waysToJoin.add(way);
555 }
556 }
557
558 for (JoinedWay jw: joinWays(waysToJoin)) {
559 result.add(new PolyData(jw));
560 if (!jw.isClosed()) {
561 openEnds.add(jw.getFirstNode());
562 openEnds.add(jw.getLastNode());
563 }
564 }
565 }
566
567 /**
568 * Attempt to combine the ways in the list if they share common end nodes
569 * @param waysToJoin The ways to join
570 * @return A collection of {@link JoinedWay} objects indicating the possible join of those ways
571 */
572 public static Collection<JoinedWay> joinWays(Collection<Way> waysToJoin) {
573 final Collection<JoinedWay> result = new ArrayList<>();
574 final Way[] joinArray = waysToJoin.toArray(new Way[0]);
575 int left = waysToJoin.size();
576 while (left > 0) {
577 boolean selected = false;
578 List<Node> nodes = null;
579 Set<Long> wayIds = new HashSet<>();
580 boolean joined = true;
581 while (joined && left > 0) {
582 joined = false;
583 for (int i = 0; i < joinArray.length && left != 0; ++i) {
584 Way c = joinArray[i];
585 if (c != null && c.isEmpty()) {
586 joinArray[i] = null;
587 left--;
588 } else if (c != null && !c.isEmpty()) {
589 if (nodes == null) {
590 // new ring
591 selected = c.isSelected();
592 joinArray[i] = null;
593 --left;
594 nodes = new ArrayList<>(c.getNodes());
595 wayIds.add(c.getUniqueId());
596 } else {
597 int cl = c.getNodesCount() - 1;
598 int nl = nodes.size() - 1;
599 int mode = 0;
600 if (nodes.get(nl) == c.getNode(0)) {
601 mode = 21;
602 } else if (nodes.get(0) == c.getNode(cl)) {
603 mode = 12;
604 } else if (nodes.get(0) == c.getNode(0)) {
605 mode = 11;
606 } else if (nodes.get(nl) == c.getNode(cl)) {
607 mode = 22;
608 }
609 if (mode != 0) {
610 // found a connection
611 joinArray[i] = null;
612 joined = true;
613 if (c.isSelected()) {
614 selected = true;
615 }
616 --left;
617 if (mode == 21) {
618 nodes.addAll(c.getNodes().subList(1, cl + 1));
619 } else if (mode == 12) {
620 nodes.addAll(0, c.getNodes().subList(0, cl));
621 } else {
622 ArrayList<Node> reversed = new ArrayList<>(c.getNodes());
623 Collections.reverse(reversed);
624 if (mode == 22) {
625 nodes.addAll(reversed.subList(1, cl + 1));
626 } else /* mode == 11 */ {
627 nodes.addAll(0, reversed.subList(0, cl));
628 }
629 }
630 wayIds.add(c.getUniqueId());
631 }
632 }
633 }
634 }
635 }
636
637 if (nodes != null) {
638 result.add(new JoinedWay(nodes, wayIds, selected));
639 }
640 }
641
642 return result;
643 }
644
645 /**
646 * Find a matching outer polygon for the inner one
647 * @param inner The inner polygon to search the outer for
648 * @param outerPolygons The possible outer polygons
649 * @return The outer polygon that was found or <code>null</code> if none was found.
650 */
651 public PolyData findOuterPolygon(PolyData inner, List<PolyData> outerPolygons) {
652 // First try to test only bbox, use precise testing only if we don't get unique result
653 Rectangle2D innerBox = inner.getBounds();
654 PolyData insidePolygon = null;
655 PolyData intersectingPolygon = null;
656 int insideCount = 0;
657 int intersectingCount = 0;
658
659 for (PolyData outer: outerPolygons) {
660 if (outer.getBounds().contains(innerBox)) {
661 insidePolygon = outer;
662 insideCount++;
663 } else if (outer.getBounds().intersects(innerBox)) {
664 intersectingPolygon = outer;
665 intersectingCount++;
666 }
667 }
668
669 if (insideCount == 1)
670 return insidePolygon;
671 else if (intersectingCount == 1)
672 return intersectingPolygon;
673
674 PolyData result = null;
675 for (PolyData combined : outerPolygons) {
676 if (combined.contains(inner.poly) != Intersection.OUTSIDE
677 && (result == null || result.contains(combined.poly) == Intersection.INSIDE)) {
678 result = combined;
679 }
680 }
681 return result;
682 }
683
684 private void addInnerToOuters(List<PolyData> innerPolygons, List<PolyData> outerPolygons) {
685 if (innerPolygons.isEmpty()) {
686 combinedPolygons.addAll(outerPolygons);
687 } else if (outerPolygons.size() == 1) {
688 PolyData combinedOuter = new PolyData(outerPolygons.get(0));
689 for (PolyData inner: innerPolygons) {
690 combinedOuter.addInner(inner);
691 }
692 combinedPolygons.add(combinedOuter);
693 } else {
694 for (PolyData outer: outerPolygons) {
695 combinedPolygons.add(new PolyData(outer));
696 }
697
698 for (PolyData pdInner: innerPolygons) {
699 Optional.ofNullable(findOuterPolygon(pdInner, combinedPolygons)).orElseGet(() -> outerPolygons.get(0))
700 .addInner(pdInner);
701 }
702 }
703 }
704
705 /**
706 * Replies the list of outer ways.
707 * @return the list of outer ways
708 */
709 public List<Way> getOuterWays() {
710 return Collections.unmodifiableList(outerWays);
711 }
712
713 /**
714 * Replies the list of inner ways.
715 * @return the list of inner ways
716 */
717 public List<Way> getInnerWays() {
718 return Collections.unmodifiableList(innerWays);
719 }
720
721 /**
722 * Replies the list of combined polygons.
723 * @return the list of combined polygons
724 */
725 public List<PolyData> getCombinedPolygons() {
726 return Collections.unmodifiableList(combinedPolygons);
727 }
728
729 /**
730 * Replies the list of inner polygons.
731 * @return the list of inner polygons
732 */
733 public List<PolyData> getInnerPolygons() {
734 final List<PolyData> innerPolygons = new ArrayList<>();
735 createPolygons(innerWays, innerPolygons);
736 return innerPolygons;
737 }
738
739 /**
740 * Replies the list of outer polygons.
741 * @return the list of outer polygons
742 */
743 public List<PolyData> getOuterPolygons() {
744 final List<PolyData> outerPolygons = new ArrayList<>();
745 createPolygons(outerWays, outerPolygons);
746 return outerPolygons;
747 }
748
749 /**
750 * Returns the start and end node of non-closed rings.
751 * @return the start and end node of non-closed rings.
752 */
753 public List<Node> getOpenEnds() {
754 return Collections.unmodifiableList(openEnds);
755 }
756}
Note: See TracBrowser for help on using the repository browser.