001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.visitor.paint.relations;
003
004import java.awt.geom.Path2D;
005import java.awt.geom.PathIterator;
006import java.awt.geom.Rectangle2D;
007import java.util.ArrayList;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.HashSet;
011import java.util.Iterator;
012import java.util.List;
013import java.util.Optional;
014import java.util.Set;
015
016import org.openstreetmap.josm.data.coor.EastNorth;
017import org.openstreetmap.josm.data.osm.DataSet;
018import org.openstreetmap.josm.data.osm.Node;
019import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
020import org.openstreetmap.josm.data.osm.Relation;
021import org.openstreetmap.josm.data.osm.RelationMember;
022import org.openstreetmap.josm.data.osm.Way;
023import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
024import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
025import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
026import org.openstreetmap.josm.data.projection.Projection;
027import org.openstreetmap.josm.data.projection.ProjectionRegistry;
028import org.openstreetmap.josm.spi.preferences.Config;
029import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
030import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
031import org.openstreetmap.josm.tools.Geometry;
032import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter;
033import org.openstreetmap.josm.tools.Logging;
034
035/**
036 * Multipolygon data used to represent complex areas, see <a href="https://wiki.openstreetmap.org/wiki/Relation:multipolygon">wiki</a>.
037 * @since 2788
038 */
039public class Multipolygon {
040
041    /** preference key for a collection of roles which indicate that the respective member belongs to an
042     * <em>outer</em> polygon. Default is <code>outer</code>.
043     */
044    public static final String PREF_KEY_OUTER_ROLES = "mappaint.multipolygon.outer.roles";
045
046    /** preference key for collection of role prefixes which indicate that the respective
047     *  member belongs to an <em>outer</em> polygon. Default is empty.
048     */
049    public static final String PREF_KEY_OUTER_ROLE_PREFIXES = "mappaint.multipolygon.outer.role-prefixes";
050
051    /** preference key for a collection of roles which indicate that the respective member belongs to an
052     * <em>inner</em> polygon. Default is <code>inner</code>.
053     */
054    public static final String PREF_KEY_INNER_ROLES = "mappaint.multipolygon.inner.roles";
055
056    /** preference key for collection of role prefixes which indicate that the respective
057     *  member belongs to an <em>inner</em> polygon. Default is empty.
058     */
059    public static final String PREF_KEY_INNER_ROLE_PREFIXES = "mappaint.multipolygon.inner.role-prefixes";
060
061    /**
062     * <p>Kind of strategy object which is responsible for deciding whether a given
063     * member role indicates that the member belongs to an <em>outer</em> or an
064     * <em>inner</em> polygon.</p>
065     *
066     * <p>The decision is taken based on preference settings, see the four preference keys
067     * above.</p>
068     */
069    private static class MultipolygonRoleMatcher implements PreferenceChangedListener {
070        private final List<String> outerExactRoles = new ArrayList<>();
071        private final List<String> outerRolePrefixes = new ArrayList<>();
072        private final List<String> innerExactRoles = new ArrayList<>();
073        private final List<String> innerRolePrefixes = new ArrayList<>();
074
075        private void initDefaults() {
076            outerExactRoles.clear();
077            outerRolePrefixes.clear();
078            innerExactRoles.clear();
079            innerRolePrefixes.clear();
080            outerExactRoles.add("outer");
081            innerExactRoles.add("inner");
082        }
083
084        private static void setNormalized(Collection<String> literals, List<String> target) {
085            target.clear();
086            for (String l: literals) {
087                if (l == null) {
088                    continue;
089                }
090                l = l.trim();
091                if (!target.contains(l)) {
092                    target.add(l);
093                }
094            }
095        }
096
097        private void initFromPreferences() {
098            initDefaults();
099            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}