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

Last change on this file since 15123 was 15123, checked in by GerdP, 5 years ago

improve javadoc: document some problems with intersection test PolyData.contains()

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