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;
034import org.openstreetmap.josm.tools.Utils;
035
036/**
037 * Multipolygon data used to represent complex areas, see <a href="https://wiki.openstreetmap.org/wiki/Relation:multipolygon">wiki</a>.
038 * @since 2788
039 */
040public class Multipolygon {
041
042    /** preference key for a collection of roles which indicate that the respective member belongs to an
043     * <em>outer</em> polygon. Default is <code>outer</code>.
044     */
045    public static final String PREF_KEY_OUTER_ROLES = "mappaint.multipolygon.outer.roles";
046
047    /** preference key for collection of role prefixes which indicate that the respective
048     *  member belongs to an <em>outer</em> polygon. Default is empty.
049     */
050    public static final String PREF_KEY_OUTER_ROLE_PREFIXES = "mappaint.multipolygon.outer.role-prefixes";
051
052    /** preference key for a collection of roles which indicate that the respective member belongs to an
053     * <em>inner</em> polygon. Default is <code>inner</code>.
054     */
055    public static final String PREF_KEY_INNER_ROLES = "mappaint.multipolygon.inner.roles";
056
057    /** preference key for collection of role prefixes which indicate that the respective
058     *  member belongs to an <em>inner</em> polygon. Default is empty.
059     */
060    public static final String PREF_KEY_INNER_ROLE_PREFIXES = "mappaint.multipolygon.inner.role-prefixes";
061
062    /**
063     * <p>Kind of strategy object which is responsible for deciding whether a given
064     * member role indicates that the member belongs to an <em>outer</em> or an
065     * <em>inner</em> polygon.</p>
066     *
067     * <p>The decision is taken based on preference settings, see the four preference keys
068     * above.</p>
069     */
070    private static class MultipolygonRoleMatcher implements PreferenceChangedListener {
071        private final List<String> outerExactRoles = new ArrayList<>();
072        private final List<String> outerRolePrefixes = new ArrayList<>();
073        private final List<String> innerExactRoles = new ArrayList<>();
074        private final List<String> innerRolePrefixes = new ArrayList<>();
075
076        private void initDefaults() {
077            outerExactRoles.clear();
078            outerRolePrefixes.clear();
079            innerExactRoles.clear();
080            innerRolePrefixes.clear();
081            outerExactRoles.add("outer");
082            innerExactRoles.add("inner");
083        }
084
085        private static void setNormalized(Collection<String> literals, List<String> target) {
086            target.clear();
087            for (String l: literals) {
088                if (l == null) {
089                    continue;
090                }
091                l = l.trim();
092                if (!target.contains(l)) {
093                    target.add(l);
094                }
095            }
096        }
097
098        private void initFromPreferences() {
099            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}