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

Last change on this file was 18211, checked in by Don-vip, 3 years ago

global use of !Utils.isEmpty/isBlank

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