Subversion Repositories mkgmap

Rev

Rev 4901 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2011-2021.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */


package uk.me.parabola.mkgmap.reader.osm;

import java.awt.Rectangle;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.stream.Collectors;

import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.IsInUtil;
import uk.me.parabola.util.Java2DConverter;
import uk.me.parabola.util.MultiIdentityHashMap;
import uk.me.parabola.util.ShapeSplitter;

/**
 * Representation of an OSM Multipolygon Relation.<br/>
 * The different way of the multipolygon are joined to polygons and inner
 * polygons are cut out from the outer polygons.
 *
 * @author WanMil
 */

public class MultiPolygonRelation extends Relation {
        private static final Logger log = Logger.getLogger(MultiPolygonRelation.class);

        public static final String STYLE_FILTER_TAG = "mkgmap:stylefilter";
        public static final String STYLE_FILTER_LINE = "polyline";
        public static final String STYLE_FILTER_POLYGON = "polygon";

        /** A tag that is set with value true on each polygon that is created by the mp processing. */
        public static final short TKM_MP_CREATED = TagDict.getInstance().xlate("mkgmap:mp_created");
        private static final short TKM_MP_ROLE = TagDict.getInstance().xlate("mkgmap:mp_role");
        private static final short TKM_CACHE_AREA_SIZEKEY = TagDict.getInstance().xlate("mkgmap:cache_area_size");
       
        public static final String ROLE_OUTER = "outer";  
        public static final String ROLE_INNER = "inner";  

        private static final byte INT_ROLE_NULL = 1;
        private static final byte INT_ROLE_INNER = 2;
        private static final byte INT_ROLE_OUTER = 4;
        private static final byte INT_ROLE_BLANK = 8;
        private static final byte INT_ROLE_OTHER = 16;

        /** maps ids to ways, will be extended with joined ways */
        private final Map<Long, Way> tileWayMap; // never clear!
       

        protected List<JoinedWay> polygons;
        private Map<Long, Way> mpPolygons = new LinkedHashMap<>();

        protected JoinedWay largestOuterPolygon;
        private Long2ObjectOpenHashMap<Coord> commonCoordMap = new Long2ObjectOpenHashMap<>();
        protected Set<Way> outerWaysForLineTagging;

        private final uk.me.parabola.imgfmt.app.Area tileBounds;
        private Area tileArea;
       
        private Coord cOfG = null;
       
        // the sum of all outer polygons area size
        private double mpAreaSize;
       
        private boolean noRecalc;

        private boolean renderingFailed;
       
        /**
         * Create an instance based on an existing relation. We need to do this
         * because the type of the relation is not known until after all its tags
         * are read in.
         *
         * @param other
         *            The relation to base this one on.
         * @param wayMap
         *            Map of all ways.
         * @param bbox
         *            The bounding box of the tile
         */

        public MultiPolygonRelation(Relation other, Map<Long, Way> wayMap, uk.me.parabola.imgfmt.app.Area bbox) {
                this.tileWayMap = wayMap;
                this.tileBounds = bbox;
                // create an Area for the bbox to clip the polygons
                tileArea = Java2DConverter.createBoundsArea(tileBounds);

                setId(other.getId());
                copyTags(other);

                other.getElements().forEach(e -> addElement(e.getKey(), e.getValue()));
                if (log.isDebugEnabled()) {
                        log.debug("Constructed multipolygon", toBrowseURL(), toTagString());
                }
        }
       

        /**
         * Retrieves the center point of this multipolygon. This is set in the
         * {@link #processElements()} methods so it returns <code>null</code>
         * before that. It can also return <code>null</code> in case the
         * multipolygon could not be processed.<br/>
         * The returned point may lie outside the multipolygon area. It is just
         * the center point of it.
         *
         * @return the center point of this multipolygon (maybe <code>null</code>)
         */

        public Coord getCofG() {
                return cOfG;
        }
       
        /**
         * Retrieves the role of the given element based on the role in the MP.
         *
         * @param jw the element
         * @return either ROLE_INNER, ROLE_OUTER or null.
         */

        private static String getRole(JoinedWay jw) {
                if (jw.intRole == INT_ROLE_INNER)
                        return ROLE_INNER;
                if (jw.intRole == INT_ROLE_OUTER)
                        return ROLE_OUTER;
                return null;
        }

        /**
         * Combine a list of way segments to a list of maximally joined ways. There are
         * lots of possible ways to do this but the result should be predictable, so
         * that multiple runs of mkgmap produce the same polygons.
         *
         * @return a list of maximally joined ways
         */

        private List<JoinedWay> joinWays() {
                List<JoinedWay> joinedWays = new ArrayList<>();
                List<JoinedWay> unclosedWays = new LinkedList<>();
               
                parseElements(joinedWays, unclosedWays);
               
                if (unclosedWays.isEmpty())
                        return joinedWays;
               
                if (unclosedWays.size() > 1) {
                        // first try to combine ways in the given order
                        joinInGivenOrder(joinedWays, unclosedWays);
                }
                if (unclosedWays.size() == 1) {
                        joinedWays.add(unclosedWays.remove(0));
                }
                if (!unclosedWays.isEmpty()) {
                        // members are not fully ordered or we have unclosed rings
                        joinWithIndex(joinedWays, unclosedWays);
                }
                joinedWays.addAll(unclosedWays);
                if(log.isInfoEnabled()) {
                        for (JoinedWay jw : joinedWays) {
                                if (Integer.bitCount(jw.intRole) > 1) {
                                        log.info("Joined polygon ways have different roles", this.toBrowseURL(), jw.toString());
                                }
                        }
                }
                return joinedWays;
        }

        /**
         * Go through list of elements, do some basic checks and separate the ways into
         * closed and unclosed ways. The (last) label node is used to set cOfG.
         *
         * @param closedWays   list to which closed ways are added
         * @param unclosedWays list to which unclosed ways are added
         */

        private void parseElements(List<JoinedWay> closedWays, List<JoinedWay> unclosedWays) {
                Map<Long, Way> dupCheck = new HashMap<>();

                for (Map.Entry<String, Element> entry : getElements()) {
                        String role = entry.getKey();
                        Element el = entry.getValue();
                        if (el instanceof Way) {
                                Way wayEl = (Way) el;
                                if (dupCheck.put(wayEl.getId(), wayEl) != null) {
                                        log.warn("repeated way member with id", el.getId(), "is ignored in multipolygon relation", toBrowseURL());
                                } else if (wayEl.getPoints().size() <= 1) {
                                        log.warn("Way", wayEl, "has", wayEl.getPoints().size(),
                                                         "points and cannot be used for the multipolygon", toBrowseURL());
                                } else {
                                        JoinedWay jw = new JoinedWay(wayEl, role);
                                        if (jw.intRole == INT_ROLE_OTHER)
                                                log.warn("Way role invalid", role, el.toBrowseURL(),
                                                                 "in multipolygon", toBrowseURL(), toTagString());
                                        if (wayEl.isClosedInOSM() && !wayEl.hasIdenticalEndPoints() && !wayEl.isComplete()) {
                                                // the way is closed in planet but some points are missing in this tile
                                                // we can close it artificially, it is very likely outside of the tile bounds
                                                if (log.isDebugEnabled())
                                                        log.debug("Close incomplete but closed polygon:", wayEl);
                                                jw.closeWayArtificially();
                                        }
                                        if (jw.hasIdenticalEndPoints())
                                                closedWays.add(jw);
                                        else {
                                                unclosedWays.add(jw);
                                        }
                                }
                        } else if (el instanceof Node) {
                                if ("label".equals(role))
                                        cOfG = ((Node) el).getLocation();
                                else if (!"admin_centre".equals(role))
                                        log.warn("Node with unknown role is ignored", role, el.toBrowseURL(),
                                                         "in multipolygon", toBrowseURL(), toTagString());
                        } else {
                                log.warn("Non Way/Node member with role is ignored", role, el.toBrowseURL(),
                                                 "in multipolygon", toBrowseURL(), toTagString());
                        }
                }
        }

        /**
         * Combines ways in the given order. Closed ways are added to closedWays, unclosed ways remain in unclosed.  
         * Stops when two ways cannot be joined in the given order.
         * @param closedWays
         * @param unclosedWays
         */

        private void joinInGivenOrder(List<JoinedWay> closedWays, List<JoinedWay> unclosedWays) {
                JoinedWay work = null;
                while (unclosedWays.size() > 1) {
                        if (work == null)
                                work = unclosedWays.get(0);
                        if (!work.canJoin(unclosedWays.get(1)))
                                break;
                        work.joinWith(unclosedWays.get(1));
                        unclosedWays.remove(1);
                        if (work.hasIdenticalEndPoints()) {
                                closedWays.add(work);
                                unclosedWays.remove(0);
                                work = null;
                        }
                }
        }

        private void joinWithIndex(List<JoinedWay> closedWays, List<JoinedWay> unclosedWays) {
                MultiIdentityHashMap<Coord, JoinedWay> index = new MultiIdentityHashMap<>();
                unclosedWays.forEach(jw -> {
                        index.add(jw.getFirstPoint(), jw);
                        index.add(jw.getLastPoint(), jw);
                });

                List<JoinedWay> finishedUnclosed = new ArrayList<>();
                while (!unclosedWays.isEmpty()) {
                        JoinedWay joinWay = unclosedWays.remove(0);

                        List<JoinedWay> candidates = index.get(joinWay.getLastPoint());
                        if (candidates.size() != 2) {
                                candidates = index.get(joinWay.getFirstPoint());
                        }
                        if (candidates.size() <= 1) {
                                // cannot join further
                                finishedUnclosed.add(joinWay);
                                // no need to maintain index
                                continue;
                        }
                        // we will join
                        candidates.remove(joinWay);
                        JoinedWay other = candidates.get(0);
                        if (candidates.size() > 1) {
                                // we have alternatives, prefer one that closes the ring.
                                other = candidates.stream().filter(joinWay::buildsRingWith).findFirst().orElse(other);
                        }
                       
                        // maintain index, we don't know which node is removed by the joining
                        index.removeMapping(other.getFirstPoint(), other);
                        index.removeMapping(other.getLastPoint(), other);
                        index.removeMapping(joinWay.getFirstPoint(), joinWay);
                        index.removeMapping(joinWay.getLastPoint(), joinWay);

                        unclosedWays.remove(other); // needs sequential search. Could set a flag in JoinedWay instead
                        joinWay.joinWith(other);

                        if (joinWay.hasIdenticalEndPoints()) {
                                closedWays.add(joinWay);
                        } else {
                                index.add(joinWay.getFirstPoint(), joinWay);
                                index.add(joinWay.getLastPoint(), joinWay);
                                unclosedWays.add(0, joinWay);
                        }
                }
                unclosedWays.addAll(finishedUnclosed);
        }

        /**
         * Try to close unclosed way.
         *
         * @param way the joined way
         *
         */

        private void tryCloseSingleWays(JoinedWay way) {
                if (way.hasIdenticalEndPoints() || way.getPoints().size() < 3)
                        return;
               
                Coord p1 = way.getFirstPoint();
                Coord p2 = way.getLastPoint();

                if ((p1.getLatitude() <= tileBounds.getMinLat() && p2.getLatitude() <= tileBounds.getMinLat())
                                || (p1.getLatitude() >= tileBounds.getMaxLat() && p2.getLatitude() >= tileBounds.getMaxLat())
                                || (p1.getLongitude() <= tileBounds.getMinLong() && p2.getLongitude() <= tileBounds.getMinLong())
                                || (p1.getLongitude() >= tileBounds.getMaxLong() && p2.getLongitude() >= tileBounds.getMaxLong())) {
                        // both points lie outside the bbox or on the bbox and
                        // they are on the same side of the bbox
                        // so just close them without worrying about if
                        // they intersect itself because the intersection also
                        // is outside the bbox
                        way.closeWayArtificially();
                        log.info("Endpoints of way", way, "are both outside the bbox. Closing it directly.");
                        return;
                }
               
                // calc the distance to close
                double closeDist = way.getFirstPoint().distance(way.getLastPoint());
                if (closeDist > getMaxCloseDist())
                        return;
                       
                // We may get here with boundary preparer, for example when a country extract doesn't contain the
                // complete data. It is assumed that the country border is very close to the cutting polygon that was
                // used for the country extract.
                Line2D closingLine = new Line2D.Double(p1.getHighPrecLon(),
                                p1.getHighPrecLat(), p2.getHighPrecLon(), p2.getHighPrecLat());

                boolean intersects = false;
                Coord lastPoint = null;
                // don't use the first and the last point
                // the closing line can intersect only in one point or complete.
                // Both isn't interesting for this check
                for (Coord thisPoint : way.getPoints().subList(1, way.getPoints().size() - 1)) {
                        if (lastPoint != null && closingLine.intersectsLine(lastPoint.getHighPrecLon(), lastPoint.getHighPrecLat(),
                                        thisPoint.getHighPrecLon(), thisPoint.getHighPrecLat())) {
                                intersects = true;
                                break;
                        }
                        lastPoint = thisPoint;
                }

                if (!intersects) {
                        // close the polygon
                        // the new way segment does not intersect the rest of the polygon
                        if (log.isInfoEnabled()) {
                                log.info("Closing way", way);
                                log.info("from", way.getFirstPoint().toOSMURL());
                                log.info("to", way.getLastPoint().toOSMURL());
                        }
                        // mark this ways as artificially closed
                        way.closeWayArtificially();
                }
        }

        private static class ConnectionData {
                Coord c1;
                Coord c2;
                JoinedWay w1;
                JoinedWay w2;
                // sometimes the connection of both points cannot be done directly but with an intermediate point
                Coord imC;
                double distance;
        }

        /**
         * Try to connect pairs of ways to closed rings or a single way by adding a
         * point outside of the tileBounds.
         *
         * @param allWays     list of ways
         * @param onlyOutside if true, only connect ways outside of the tileBounds
         * @return true if anything was closed
         */

        private boolean connectUnclosedWays(List<JoinedWay> allWays, boolean onlyOutside) {
                List<JoinedWay> unclosed = allWays.stream().filter(w->!w.hasEqualEndPoints()).collect(Collectors.toList());
               
                // try to connect ways lying outside or on the bbox
                if (!unclosed.isEmpty()) {
                        log.debug("Checking", unclosed.size(), "unclosed ways for connections outside the bbox");
                        Map<Coord, JoinedWay> openEnds = new IdentityHashMap<>();
                       
                        // check all ways for endpoints outside or on the bbox
                        for (JoinedWay w : unclosed) {
                                for (Coord e : Arrays.asList(w.getFirstPoint(), w.getLastPoint())) {
                                        if (!onlyOutside) {
                                                openEnds.put(e, w);
                                        } else {
                                                if (!tileBounds.insideBoundary(e)) {
                                                        log.debug("Point", e, "of way", w.getId(), "outside bbox");
                                                        openEnds.put(e, w);
                                                }
                                        }
                                }
                        }

                        if (openEnds.size() < 2) {
                                log.debug(openEnds.size(), "point outside the bbox. No connection possible.");
                                return false;
                        }
                       
                        List<ConnectionData> coordPairs = new ArrayList<>();
                        ArrayList<Coord> coords = new ArrayList<>(openEnds.keySet());
                        for (int i = 0; i < coords.size(); i++) {
                                for (int j = i + 1; j < coords.size(); j++) {
                                        ConnectionData cd = new ConnectionData();
                                        cd.c1 = coords.get(i);
                                        cd.c2 = coords.get(j);
                                        cd.w1 = openEnds.get(cd.c1);                                   
                                        cd.w2 = openEnds.get(cd.c2);
                                       
                                        if (!onlyOutside && cd.w1 == cd.w2)
                                                continue; // was already tested in tryCloseSingleWays()
                                       
                                        if (onlyOutside && lineCutsBbox(cd.c1, cd.c2)) {
                                                // Check if the way can be closed with one additional point
                                                // outside the bounding box.
                                                // The additional point is combination of the coords of both endpoints.
                                                // It works if the lines from the endpoints to the additional point does
                                                // not cut the bounding box.
                                                // This can be removed when the splitter guarantees to provide logical complete
                                                // multi-polygons.
                                                Coord edgePoint1 = new Coord(cd.c1.getLatitude(), cd.c2.getLongitude());
                                                Coord edgePoint2 = new Coord(cd.c2.getLatitude(), cd.c1.getLongitude());
                                               
                                                List<Coord> possibleEdges = new ArrayList<>();
                                                if (!lineCutsBbox(cd.c1, edgePoint1) && !lineCutsBbox(edgePoint1, cd.c2)) {
                                                        possibleEdges.add(edgePoint1);
                                                }
                                                if (!lineCutsBbox(cd.c1, edgePoint2) && !lineCutsBbox(edgePoint2, cd.c2)) {
                                                        possibleEdges.add(edgePoint2);
                                                }
                                                if (possibleEdges.size() != 1) {
                                                        // both endpoints are on opposite sides of the bounding box
                                                        // automatically closing such points would create wrong polygons in most cases
                                                        continue;
                                                }
                                                cd.imC = possibleEdges.get(0);
                                                cd.distance = cd.c1.distance(cd.imC) + cd.imC.distance(cd.c2);
                                        } else {
                                                cd.distance = cd.c1.distance(cd.c2);
                                        }
                                        coordPairs.add(cd);
                                }
                        }

                        if (coordPairs.isEmpty()) {
                                log.debug("All potential connections cross the bbox. No connection possible.");
                                return false;
                        }
                        // retrieve the connection with the minimum distance
                        ConnectionData minCon = Collections.min(coordPairs, (o1, o2) -> Double.compare(o1.distance, o2.distance));

                        if (onlyOutside || minCon.distance < getMaxCloseDist()) {
                                if (minCon.w1 == minCon.w2) {
                                        log.debug("Close a gap in way", minCon.w1);
                                        if (minCon.imC != null)
                                                minCon.w1.getPoints().add(minCon.imC);
                                        minCon.w1.closeWayArtificially();
                                } else {
                                        log.debug("Connect", minCon.w1, "with", minCon.w2);

                                        if (minCon.w1.getFirstPoint() == minCon.c1) {
                                                Collections.reverse(minCon.w1.getPoints());
                                        }
                                        if (minCon.w2.getFirstPoint() != minCon.c2) {
                                                Collections.reverse(minCon.w2.getPoints());
                                        }

                                        minCon.w1.getPoints().addAll(minCon.w2.getPoints());
                                        minCon.w1.addWay(minCon.w2);
                                        allWays.remove(minCon.w2);
                                }
                                return true;
                        }
                }
                return false;
        }
       
        /**
         * Removes all non closed ways from the given list.
         * <code>!{@link Way#hasIdenticalEndPoints()}</code>)
         *
         * @param wayList
         *            list of ways
         */

        private void removeUnclosedWays(List<JoinedWay> wayList) {
                Iterator<JoinedWay> it = wayList.iterator();
                boolean firstWarn = true;
                while (it.hasNext()) {
                        JoinedWay jw = it.next();
                        if (!jw.hasIdenticalEndPoints()) {
                                // warn only if the way bbox intersects the bounding box
                                if (jw.getArea().intersects(tileBounds)) {
                                        if (firstWarn) {
                                                log.warn(
                                                        "Cannot join the following ways to closed polygons. Multipolygon",
                                                        toBrowseURL(), toTagString());
                                                firstWarn = false;
                                        }
                                        logWayURLs(Level.WARNING, "- way:", jw);
                                        logFakeWayDetails(Level.WARNING, jw);
                                        String role = getRole(jw);
                                        if (role == null || ROLE_OUTER.equals(role)) {
                                                // anyhow add the ways to the list for line tagging
                                                outerWaysForLineTagging.addAll(jw.getOriginalWays());
                                        }
                                }

                                it.remove();
                        }
                }
        }

        /**
         * Removes all ways that are completely outside the bounding box.
         * This reduces error messages from problems on the tile bounds.
         * @param wayList list of ways
         */

        private void removeWaysOutsideBbox(List<JoinedWay> wayList) {
                ListIterator<JoinedWay> wayIter = wayList.listIterator();
                while (wayIter.hasNext()) {
                        JoinedWay w = wayIter.next();
                       
                        if (isFullyOutsideBBox(w)) {
                                if (log.isDebugEnabled()) {
                                        if (w.originalWays.size() == 1) {
                                                log.debug(this.getId(), ": Ignoring way", w.originalWays.get(0).getId(),
                                                                "because it is completely outside the bounding box.");
                                        } else {
                                                log.debug(this.getId(), ": Ignoring joined ways",
                                                                w.originalWays.stream().map(way -> Long.toString(way.getId()))
                                                                                .collect(Collectors.joining(",")),
                                                                "because they are completely outside the bounding box.");
                                        }
                                }
                                wayIter.remove();
                        }
                }
        }

        private boolean isFullyOutsideBBox(JoinedWay w) {
                if (!w.getBounds().intersects(tileArea.getBounds())) {
                        return true;
                }
               
                // check if the polygon bbox contains the complete tile bounds
                if (w.getBounds().contains(tileArea.getBounds())) {
                        return false;
                }
               
                // check if any point is inside tile bounds
                if (w.getPoints().stream().anyMatch(tileBounds::contains))
                        return false;

                // check if any line segment of the polygon crosses the tile bounds
                for (int i = 0; i < w.getPoints().size() - 1; i++) {
                        if (lineCutsBbox(w.getPoints().get(i), w.getPoints().get(i + 1))) {
                                return false;
                        }
                }
                return true;
        }

        /**
         * Process the ways in this relation. Tries to join the ways to closed rings and
         * detect inner/outer status and calls methods to process them.
         */

        public final void processElements() {
                log.info("Processing multipolygon", toBrowseURL());
               
                // check if it makes sense to process the mp
                if (!isUsable()) {
                        log.info("Do not process multipolygon", getId(), "because it has no style relevant tags.");
                        return;
                }
                polygons = buildRings();
                if (polygons.isEmpty())
                        return;
               
                // trigger setting area before start cutting...
                // do like this to disguise function with side effects
                polygons.forEach(jw -> jw.setFullArea(jw.getFullArea()));
               
                if (polygons.stream().allMatch(jw -> jw.intRole == INT_ROLE_INNER || jw.intRole == INT_ROLE_OTHER)) {
                        log.warn("Multipolygon", toBrowseURL(),
                                "does not contain any way tagged with role=outer or empty role.");
                        cleanup();
                        return;
                }
               
                //TODO: trunk uses more complex logic that also takes the inners into account
                largestOuterPolygon = getLargest(polygons);
               
                List<List<JoinedWay>> partitions = new ArrayList<>();
                if ("boundary".equals(getTag("type")) || this.noPartitioning()) {
                        partitions.add(polygons);
                } else {         
                        divideLargest(polygons, partitions, 0);
                }
                for (List<JoinedWay> some : partitions) {
                        processPartition(new Partition(some));
                        if (renderingFailed)
                                break;
                }

                tagOuterWays();
               
                postProcessing();
                cleanup();
        }

        /**
         * Other Implementations can return true to process the MP as is
         * @return true if the MP should never be split before cutting.
         */

        protected boolean noPartitioning() {
                return false;
        }

        private List<JoinedWay> buildRings() {
                List<JoinedWay> polygons = joinWays();

                outerWaysForLineTagging = new HashSet<>();
               
                polygons = filterUnclosed(polygons);
               
                do {
                        polygons.forEach(this::tryCloseSingleWays);
                } while (connectUnclosedWays(polygons, assumeDataInBoundsIsComplete()));

                removeUnclosedWays(polygons);

                // now only closed ways are left => polygons only

                // check if we have at least one polygon left
                boolean hasPolygons = !polygons.isEmpty();

                removeWaysOutsideBbox(polygons);

                if (polygons.isEmpty()) {
                        // do nothing
                        if (log.isInfoEnabled()) {
                                log.info("Multipolygon", toBrowseURL(),
                                                hasPolygons ? "is completely outside the bounding box. It is not processed."
                                                                : "does not contain a closed polygon.");
                        }
                        tagOuterWays();
                        cleanup();
                }
                return polygons;
        }
       
        private static JoinedWay getLargest(List<JoinedWay> polygons) {
                double maxSize = -1;
                int maxPos = -1;
                for (int i = 0; i< polygons.size(); i++) {
                        JoinedWay closed = polygons.get(i);
                        double size = calcAreaSize(closed.getPoints());
                        if (size > maxSize) {
                                maxSize = size;
                                maxPos = i;
                        }
                }
                return polygons.get(maxPos);
        }


        /**
         * Calculate the bounds of given collection of joined ways
         * @param polygons list of polygons
         * @return the bounds
         */

        private static uk.me.parabola.imgfmt.app.Area calcBounds(Collection<JoinedWay> polygons) {
                int minLat = Integer.MAX_VALUE;
                int minLon = Integer.MAX_VALUE;
                int maxLat = Integer.MIN_VALUE;
                int maxLon = Integer.MIN_VALUE;
                for (JoinedWay jw : polygons) {
                        if (jw.minLat < minLat)
                                minLat = jw.minLat;
                        if (jw.minLon < minLon)
                                minLon = jw.minLon;
                        if (jw.maxLat > maxLat)
                                maxLat = jw.maxLat;
                        if (jw.maxLon > maxLon)
                                maxLon = jw.maxLon;
                }
                return new uk.me.parabola.imgfmt.app.Area(minLat, minLon, maxLat, maxLon);
        }


        void processPartition(Partition partition) {
                if (partition.innerEqualsOuter)
                        return;
                if (partition.outerPolygons.isEmpty()) {
                        renderingFailed = true;
                        log.error("Internal error: Failed to render " + this);
                        return;
                }
                       
                Queue<PolygonStatus> polygonWorkingQueue = new LinkedBlockingQueue<>();
               
                polygonWorkingQueue.addAll(partition.getPolygonStatus(null));
                processQueue(partition, polygonWorkingQueue);

                if (doReporting() && log.isLoggable(Level.WARNING)) {
                        partition.reportProblems();
                }

        }

        protected boolean doReporting() {
                return true;
        }


        protected boolean isUsable() {
                // TODO: Would be good to have a hook to filter unwanted MP, e.g.  
                for (Map.Entry<String, String> tagEntry : this.getTagEntryIterator()) {
                        String tagName = tagEntry.getKey();
                        // all tags are style relevant
                        // except: type and mkgmap:*
                        if (!"type".equals(tagName) && !tagName.startsWith("mkgmap:")) {
                                return true;
                        }
                }
                return false;
        }

        /**
         * Should return true if extra ways with style filter {@code STYLE_FILTER_LINE}
         * should be added to the {@code tileWayMap}. Overwrite if those ways are not needed.
         * @return true if extra ways with style filter {@code STYLE_FILTER_LINE}
         * should be added to the {@code tileWayMap}
         */

        protected boolean needsWaysForOutlines() {
                return true;
        }

        protected boolean assumeDataInBoundsIsComplete() {
                // we assume that data inside tile boundaries is complete
                return true;
        }

        private void tagOuterWays() {
                if (outerWaysForLineTagging.isEmpty())
                        return;
               
                final Way patternWayForLineCopies;
                if (needsWaysForOutlines()) {
                        // create pattern way with tags for outline of this multipolygon
                        patternWayForLineCopies = new Way(0);
                        patternWayForLineCopies.copyTags(this);
                        patternWayForLineCopies.deleteTag("type");
                        patternWayForLineCopies.addTag(STYLE_FILTER_TAG, STYLE_FILTER_LINE);
                        patternWayForLineCopies.addTag(TKM_MP_CREATED, "true");
                        if (needsAreaSizeTag()) {
                                patternWayForLineCopies.addTag(TKM_CACHE_AREA_SIZEKEY, getAreaSizeString());
                        }
                } else {
                        patternWayForLineCopies = null;
                }
               
                // Go through all original outer ways, create a copy if wanted, tag them
                // with the mp tags and mark them only to be used for polyline processing
                // This enables the style file to decide if the polygon information or
                // the simple line information should be used.
               
                for (Way orgOuterWay : outerWaysForLineTagging) {
                        if (patternWayForLineCopies != null) {
                                Way lineTagWay =  new Way(getOriginalId(), orgOuterWay.getPoints());
                                lineTagWay.markAsGeneratedFrom(this);
                                lineTagWay.copyTags(patternWayForLineCopies);
                                if (log.isDebugEnabled())
                                        log.debug("Add line way", lineTagWay.getId(), lineTagWay.toTagString());
                                tileWayMap.put(lineTagWay.getId(), lineTagWay);
                        }
                       
                        for (Entry<String, String> tag : this.getTagEntryIterator()) {
                                // mark the tag for removal in the original way if it has the same value
                                if (tag.getValue().equals(orgOuterWay.getTag(tag.getKey()))) {
                                        markTagsForRemovalInOrgWays(orgOuterWay, tag.getKey());
                                }
                        }
                }
        }

        /**
         * Filter unclosed ways which have one or both end points outside of the tile bounds.
         * @param polygons the list of ways to filter
         * @return the original list if {@link allowCloseOutsideBBox} returns false, else the filtered list
         */

        private List<JoinedWay> filterUnclosed(List<JoinedWay> polygons) {
                if (assumeDataInBoundsIsComplete())
                        return polygons;
                return polygons.stream().filter(w -> {
                        Coord first = w.getFirstPoint();
                        Coord last = w.getLastPoint();
                        return first == last || tileBounds.contains(first) && tileBounds.contains(last);
                }).collect(Collectors.toList());
        }

        /**
         * The main routine to cut or split the rings of one partition containing a list of polygons
         * @param partition the partition
         * @param polygonWorkingQueue the queue that contains the initial outer rings
         */

        protected void processQueue(Partition partition, Queue<PolygonStatus> polygonWorkingQueue) {
               
                while (!polygonWorkingQueue.isEmpty()) {

                        // the polygon is not contained by any other unfinished polygon
                        PolygonStatus currentPolygon = polygonWorkingQueue.poll();

                        // this polygon is now processed and should not be used by any
                        // further step
                        partition.markFinished(currentPolygon);

                        List<PolygonStatus> holes = partition.getPolygonStatus(currentPolygon);

                        // these polygons must all be checked for holes
                        polygonWorkingQueue.addAll(holes);

                        if (currentPolygon.outer) {
                                // add the original ways to the list of ways that get the line tags of the mp
                                // the joined ways may be changed by the auto closing algorithm
                                outerWaysForLineTagging.addAll(currentPolygon.polygon.getOriginalWays());
                        }
                       
                        // check if the polygon is an outer polygon or
                        // if there are some holes
                        boolean processPolygon = currentPolygon.outer || !holes.isEmpty();

                        if (processPolygon) {
                                List<Way> singularOuterPolygons;
                                if (holes.isEmpty()) {
                                        Way w = new Way(currentPolygon.polygon.getId(), currentPolygon.polygon.getPoints());
                                        singularOuterPolygons = Collections.singletonList(w);
                                } else {
                                        List<Way> innerWays = new ArrayList<>(holes.size());
                                        for (PolygonStatus polygonHoleStatus : holes) {
                                                innerWays.add(polygonHoleStatus.polygon);
                                        }

                                        MultiPolygonCutter cutter = new MultiPolygonCutter(this, tileArea, commonCoordMap);
                                        singularOuterPolygons = cutter.cutOutInnerPolygons(currentPolygon.polygon, innerWays);
                                        if (currentPolygon.outer) {
                                                singularOuterPolygons.forEach(s -> s.setMpRel(this));
                                        }
                                }
                               
                                if (!singularOuterPolygons.isEmpty()) {
                                        // handle the tagging
                                        if (currentPolygon.outer) {
                                                // use the tags of the multipolygon
                                                for (Way p : singularOuterPolygons) {
                                                        // overwrite all tags
                                                        p.copyTags(this);
                                                        p.deleteTag("type");
                                                }
                                        } else {
                                                // we have a nested MP with one or more outer rings inside an inner ring
                                                // use the tags of the original ways
                                                currentPolygon.polygon.mergeTagsFromOrgWays();
                                                for (Way p : singularOuterPolygons) {
                                                        // overwrite all tags
                                                        p.copyTags(currentPolygon.polygon);
                                                }
                                                // remove the current polygon tags in its original ways
                                                markTagsForRemovalInOrgWays(currentPolygon.polygon);
                                        }
                               
                                        long fullArea = currentPolygon.polygon.getFullArea();
                                        for (Way mpWay : singularOuterPolygons) {
                                                // put the cut out polygons to the
                                                // final way map
                                                if (log.isDebugEnabled())
                                                        log.debug(mpWay.getId(), mpWay.toTagString());
                                       
                                                mpWay.setFullArea(fullArea);
                                                // mark this polygons so that only polygon style rules are applied
                                                mpWay.addTag(STYLE_FILTER_TAG, STYLE_FILTER_POLYGON);
                                                mpWay.addTag(TKM_MP_CREATED, "true");
                                               
                                                if (currentPolygon.outer) {
                                                        mpWay.addTag(TKM_MP_ROLE, ROLE_OUTER);
                                                        if (needsAreaSizeTag())
                                                                mpAreaSize += calcAreaSize(mpWay.getPoints());
                                                } else {
                                                        mpWay.addTag(TKM_MP_ROLE, ROLE_INNER);
                                                }
                                               
                                                mpPolygons.put(mpWay.getId(), mpWay);
                                        }
                                }
                        }
                }
        }


        protected double getMaxCloseDist() {
                return Double.MAX_VALUE; // overwritten in BoundaryRelation
        }

        private String getAreaSizeString() {
                return String.format(Locale.US, "%.3f", mpAreaSize);
        }

        protected void postProcessing() {
                String mpAreaSizeStr = null;
                if (needsAreaSizeTag()) {
                        // assign the area size of the whole multipolygon to all outer polygons
                        mpAreaSizeStr = getAreaSizeString();
                        addTag(TKM_CACHE_AREA_SIZEKEY, mpAreaSizeStr);
                }

                if (!renderingFailed) {
                        for (Way w : mpPolygons.values()) {
                                String role = w.deleteTag(TKM_MP_ROLE);
                                if (mpAreaSizeStr != null && ROLE_OUTER.equals(role)) {
                                        w.addTag(TKM_CACHE_AREA_SIZEKEY, mpAreaSizeStr);
                                }
                        }
                        // copy all polygons created by the multipolygon algorithm to the global way map
                        tileWayMap.putAll(mpPolygons);
                }
                if (cOfG == null && largestOuterPolygon != null) {
                        // use the center of the largest polygon as reference point
                        cOfG = largestOuterPolygon.getCofG();
                }
                // XXX: maybe keep the cOfg data from a label node?
                if (largestOuterPolygon == null)
                        cOfG = null;
        }
       
        protected void cleanup() {
                mpPolygons = null;
                tileArea = null;
                outerWaysForLineTagging = null;
                commonCoordMap = null;
        }

        /**
         * Checks if polygon1 contains polygon2 without intersection.
         *
         * @param expectedOuter
         *            a closed way
         * @param expectedInner
         *            a 2nd closed way
         * @return true if polygon1 contains polygon2 without intersection.
         */

        private static boolean calcContains(JoinedWay expectedOuter, JoinedWay expectedInner) {
                if (!expectedOuter.hasIdenticalEndPoints()) {
                        return false;
                }
                // check if the bounds of polygon2 are completely inside/enclosed the bounds
                // of polygon1
                if (!expectedOuter.getBounds().contains(expectedInner.getBounds())) {
                        return false;
                }
               
//              Coord test = expectedInner.getPointInside();
//              if (test != null) {
//                      // we know that point test is inside expectedInner
//                      int quick = IsInUtil.isPointInShape(test, expectedOuter.getPoints());
//                      // if point is ON we can assume that a part of the inner is OUT
//                      return quick == IsInUtil.IN;
//              }
               
                int x = IsInUtil.isLineInShape(expectedInner.getPoints(), expectedOuter.getPoints(), expectedInner.getArea());
                return (x & IsInUtil.OUT) == 0;
        }

        private boolean lineCutsBbox(Coord p1, Coord p2) {
                Coord nw = new Coord(tileBounds.getMaxLat(), tileBounds.getMinLong());
                Coord sw = new Coord(tileBounds.getMinLat(), tileBounds.getMinLong());
                Coord se = new Coord(tileBounds.getMinLat(), tileBounds.getMaxLong());
                Coord ne = new Coord(tileBounds.getMaxLat(), tileBounds.getMaxLong());
                return linesCutEachOther(nw, sw, p1, p2)
                                || linesCutEachOther(sw, se, p1, p2)
                                || linesCutEachOther(se, ne, p1, p2)
                                || linesCutEachOther(ne, nw, p1, p2);
        }

        /**
         * XXX: This code presumes that certain tests were already done!
         * Check if the line p1_1 to p1_2 cuts line p2_1 to p2_2 in two pieces and vice versa.
         * This is a form of intersection check where it is allowed that one line ends on the
         * other line or that the two lines overlap.
         * @param p11 first point of line 1
         * @param p12 second point of line 1
         * @param p21 first point of line 2
         * @param p22 second point of line 2
         * @return true if both lines intersect somewhere in the middle of each other
         */

        private static boolean linesCutEachOther(Coord p11, Coord p12, Coord p21, Coord p22) {
                long width1 = (long) p12.getHighPrecLon() - p11.getHighPrecLon();
                long width2 = (long) p22.getHighPrecLon() - p21.getHighPrecLon();

                long height1 = (long) p12.getHighPrecLat() - p11.getHighPrecLat();
                long height2 = (long) p22.getHighPrecLat() - p21.getHighPrecLat();

                long denominator = ((height2 * width1) - (width2 * height1));
                if (denominator == 0) {
                        // the lines are parallel
                        // they might overlap but this is ok for this test
                        return false;
                }
               
                long x1Mx3 = (long) p11.getHighPrecLon() - p21.getHighPrecLon();
                long y1My3 = (long) p11.getHighPrecLat() - p21.getHighPrecLat();

                double isx = (double) ((width2 * y1My3) - (height2 * x1Mx3)) / denominator;
                if (isx <= 0 || isx >= 1) {
                        return false;
                }
               
                double isy = (double) ((width1 * y1My3) - (height1 * x1Mx3)) / denominator;

                return (isy > 0 &&  isy < 1);
        }

        private static void logWayURLs(Level level, String preMsg, Way way) {
                if (log.isLoggable(level)) {
                        if (way instanceof JoinedWay) {
                                if (((JoinedWay) way).getOriginalWays().isEmpty()) {
                                        log.warn("Way", way, "does not contain any original ways");
                                }
                                for (Way segment : ((JoinedWay) way).getOriginalWays()) {
                                        if (preMsg == null || preMsg.length() == 0) {
                                                log.log(level, segment.toBrowseURL());
                                        } else {
                                                log.log(level, preMsg, segment.toBrowseURL());
                                        }
                                }
                        } else {
                                if (preMsg == null || preMsg.length() == 0) {
                                        log.log(level, way.toBrowseURL());
                                } else {
                                        log.log(level, preMsg, way.toBrowseURL());
                                }
                        }
                }
        }
       
        /**
         * Logs the details of the original ways of a way with a fake id. This is
         * primarily necessary for the sea multipolygon because it consists of
         * faked ways only. In this case logging messages can be improved by the
         * start and end points of the faked ways.
         * @param logLevel the logging level
         * @param fakeWay a way composed by other ways with faked ids
         */

        private void logFakeWayDetails(Level logLevel, JoinedWay fakeWay) {
                if (!log.isLoggable(logLevel)) {
                        return;
                }
               
                // only log if this is an artificial multipolygon
                if (!FakeIdGenerator.isFakeId(getId())) {
                        return;
                }
               
                if (fakeWay.getOriginalWays().stream().noneMatch(w -> FakeIdGenerator.isFakeId(w.getId())))
                        return;
               
                // the fakeWay consists only of other faked ways
                // there should be more information about these ways
                // so that it is possible to retrieve the original
                // OSM ways
                // => log the start and end points
               
                for (Way orgWay : fakeWay.getOriginalWays()) {
                        log.log(logLevel, "Way", orgWay.getId(), "is composed of other artificial ways. Details:");
                        log.log(logLevel, " Start:", orgWay.getFirstPoint().toOSMURL());
                        if (orgWay.hasEqualEndPoints()) {
                                // the way is closed so start and end are equal - log the point in the middle of the way
                                int mid = orgWay.getPoints().size()/2;
                                log.log(logLevel, " Mid:  ", orgWay.getPoints().get(mid).toOSMURL());
                        } else {
                                log.log(logLevel, " End:  ", orgWay.getLastPoint().toOSMURL());
                        }
                }              
        }

        /**
         * Marks all tags of the original ways of the given JoinedWay for removal.
         *
         * @param way a joined way
         */

        private static void markTagsForRemovalInOrgWays(JoinedWay way) {
                for (Entry<String, String> tag : way.getTagEntryIterator()) {
                        markTagForRemovalInOrgWays(way, tag.getKey(), tag.getValue());
                }
        }

        /**
         * Mark the given tag for removal in all original ways of the given way.
         *
         * @param way      a joined way
         * @param tagKey   the tag to be removed
         * @param tagvalue the value of the tag to be removed
         */

        private static void markTagForRemovalInOrgWays(JoinedWay way, String tagKey, String tagvalue) {
                for (Way w : way.getOriginalWays()) {
                        if (w instanceof JoinedWay) {
                                // remove the tag recursively
                                markTagForRemovalInOrgWays((JoinedWay) w, tagKey, tagvalue);
                        } else if (tagvalue.equals(w.getTag(tagKey))) {
                                markTagsForRemovalInOrgWays(w, tagKey);
                        }
                }
        }
       
        /**
         * Add given tag key to the special tag which contains the list of tag keys
         * which are to be removed in MultiPolygonFinishHook.
         *
         * @param way    the way
         * @param tagKey the tag key
         */

        private static void markTagsForRemovalInOrgWays(Way way, String tagKey) {
                if (tagKey == null || tagKey.isEmpty()) {
                        return; // should not happen
                }

                String tagsToRemove = way.getTag(ElementSaver.TKM_REMOVETAGS);
               
                if (tagsToRemove == null) {
                        tagsToRemove = tagKey;
                } else if (tagKey.equals(tagsToRemove)) {
                        return;
                } else {
                        String[] keys = tagsToRemove.split(";");
                        if (Arrays.asList(keys).contains(tagKey)) {
                                return;
                        }
                        tagsToRemove += ";" + tagKey;
                }
                if (log.isDebugEnabled()) {
                        log.debug("Will remove", tagKey + "=" + way.getTag(tagKey), "from way", way.getId(), way.toTagString());
                }
                way.addTag(ElementSaver.TKM_REMOVETAGS, tagsToRemove);
        }
       
        /**
         * Flag if the area size of the mp should be calculated and added as tag.
         * @return {@code true} area size should be calculated; {@code false} area size should not be calculated
         */

        protected boolean needsAreaSizeTag() {
                return true;
        }

        protected Map<Long, Way> getTileWayMap() {
                return tileWayMap;
        }

        protected Map<Long, Way> getMpPolygons() {
                return mpPolygons;
        }

        protected uk.me.parabola.imgfmt.app.Area getTileBounds() {
                return tileBounds;
        }
       
        /**
         * Calculates a unitless number that gives a value for the size
         * of the area. The calculation does not correct to any earth
         * coordinate system. It uses the simple rectangular coordinate
         * system of garmin coordinates.
         *
         * @param polygon the points of the area
         * @return the size of the area (unitless)
         */

        public static double calcAreaSize(List<Coord> polygon) {
                if (polygon.size() < 4 || polygon.get(0) != polygon.get(polygon.size() - 1)) {
                        return 0; // line or not closed
                }
                long area = 0;
                Iterator<Coord> polyIter = polygon.iterator();
                Coord c2 = polyIter.next();
                while (polyIter.hasNext()) {
                        Coord c1 = c2;
                        c2 = polyIter.next();
                        area += (long) (c2.getHighPrecLon() + c1.getHighPrecLon())
                                        * (c1.getHighPrecLat() - c2.getHighPrecLat());
                }
                //  convert from high prec to value in map units
                double areaSize = (double) area / (2 * (1 << Coord.DELTA_SHIFT) * (1 << Coord.DELTA_SHIFT));  
                return Math.abs(areaSize);
        }

        /**
         * This is a helper class that gives access to the original
         * segments of a joined way or a string of ways. It may be unclosed.
         */

        public static final class JoinedWay extends Way {
                private final List<Way> originalWays;
                private byte intRole;  
                private boolean closedArtificially;
                private Coord pointInside;
                private boolean doPointInsideCalcs = true;

                private int minLat;
                private int maxLat;
                private int minLon;
                private int maxLon;
                private Rectangle bounds;
                private uk.me.parabola.imgfmt.app.Area area;

                public JoinedWay(Way originalWay, String givenRole) {
                        super(originalWay.getOriginalId(), originalWay.getPoints());
                        markAsGeneratedFrom(originalWay);
                        originalWays = new ArrayList<>();
                        addWay(originalWay, roleToInt(givenRole));

                        // we have to initialize the min/max values
                        Coord c0 = originalWay.getFirstPoint();
                        minLat = maxLat = c0.getLatitude();
                        minLon = maxLon = c0.getLongitude();

                        updateBounds(originalWay.getPoints());
                }

                public JoinedWay(JoinedWay other, List<Coord> points) {
                        super(other.getOriginalId(), points);
                        markAsGeneratedFrom(other);
                        originalWays = new ArrayList<>(other.getOriginalWays());
                        intRole = other.intRole;
                        closedArtificially = other.closedArtificially;
                        // we have to initialize the min/max values
                        Coord c0 = points.get(0);
                        minLat = maxLat = c0.getLatitude();
                        minLon = maxLon = c0.getLongitude();

                        updateBounds(points);
                       
                }

                private byte roleToInt(String role) {
                        if (role == null)
                                return INT_ROLE_NULL;
                        switch (role) {
                        case ROLE_INNER:
                                return INT_ROLE_INNER;
                        case ROLE_OUTER:
                                return INT_ROLE_OUTER;
                        case "":
                                return INT_ROLE_BLANK;
                        default:
                                return INT_ROLE_OTHER;
                        }
                }

                public void addPoint(int index, Coord point) {
                        getPoints().add(index, point);
                        updateBounds(point);
                }

                @Override
                public void addPoint(Coord point) {
                        super.addPoint(point);
                        updateBounds(point);
                }

                private void updateBounds(List<Coord> pointList) {
                        for (Coord c : pointList) {
                                updateBounds(c.getLatitude(), c.getLongitude());
                        }
                }

                private void updateBounds(JoinedWay other) {
                        updateBounds(other.minLat,other.minLon);
                        updateBounds(other.maxLat,other.maxLon);
                }

                private void updateBounds(int lat, int lon) {
                        if (lat < minLat) {
                                minLat = lat;
                                bounds = null;
                        } else if (lat > maxLat) {
                                maxLat = lat;
                                bounds = null;
                        }

                        if (lon < minLon) {
                                minLon = lon;
                                bounds = null;
                        } else if (lon > maxLon) {
                                maxLon = lon;
                                bounds = null;
                        }
                }
               
                private void updateBounds(Coord point) {
                        updateBounds(point.getLatitude(), point.getLongitude());
                }
               
                public Rectangle getBounds() {
                        if (bounds == null) {
                                // note that we increase the rectangle by 1 because intersects
                                // checks
                                // only the interior
                                bounds = new Rectangle(minLon - 1, minLat - 1, maxLon - minLon
                                                + 2, maxLat - minLat + 2);
                        }

                        return bounds;
                }

                public uk.me.parabola.imgfmt.app.Area getArea() {
                        if (area == null) {
                                area = new uk.me.parabola.imgfmt.app.Area(minLat, minLon, maxLat, maxLon);
                        }

                        return area;
                }

                public void addWay(Way way, int internalRole) {
                        if (way instanceof JoinedWay) {
                                originalWays.addAll(((JoinedWay) way).getOriginalWays());
                                this.intRole |= ((JoinedWay) way).intRole;
                                updateBounds((JoinedWay) way);
                        } else {
                                if (log.isDebugEnabled()) {
                                        log.debug("Joined", this.getId(), "with", way.getId());
                                }
                                this.originalWays.add(way);
                                this.intRole |= internalRole;
                        }
                }

                public void addWay(JoinedWay way) {
                        originalWays.addAll(way.originalWays);
                        this.intRole |= way.intRole;
                        updateBounds(way);
                }

                public void closeWayArtificially() {
                        addPoint(getPoints().get(0));
                        closedArtificially = true;
                }

                public boolean isClosedArtificially() {
                        return closedArtificially;
                }

                /**
                 * Get common tags of all ways.
                 * @param ways the collection of ways
                 * @return map with common tags, might be empty but will never be null
                 */

                public static Map<String, String> getMergedTags(Collection<Way> ways) {
                        Map<String, String> mergedTags = new HashMap<>();
                        boolean first = true;
                        for (Way way : ways) {
                                if (first) {
                                        // the tags of the first way are copied completely
                                        for (Map.Entry<String, String> tag : way.getTagEntryIterator()) {
                                                mergedTags.put(tag.getKey(), tag.getValue());
                                        }
                                        first = false;
                                } else {
                                        if (mergedTags.isEmpty()) {
                                                break;
                                        }
                                        // remove tags with different value
                                        mergedTags.entrySet().removeIf(tag -> {
                                                String wayTagValue = way.getTag(tag.getKey());
                                                return (wayTagValue != null && !tag.getValue().equals(wayTagValue));
                                        });
                                }
                        }
                        return mergedTags;
                }
               
                /**
                 * Tags this way with a merge of the tags of all original ways.
                 */

                public void mergeTagsFromOrgWays() {
                        if (log.isDebugEnabled()) {
                                log.debug("Way", getId(), "merge tags from", getOriginalWays().size(), "ways");
                        }
                        removeAllTags();
                       
                        Map<String, String> mergedTags = getMergedTags(getOriginalWays());
                        mergedTags.forEach(this::addTag);
                }

                public List<Way> getOriginalWays() {
                        return originalWays;
                }
               
                @Override
                public String toString() {
                        final String prefix = getId() + "(" + getPoints().size() + "P)(";
                        return getOriginalWays().stream().map(w -> w.getId() + "[" + w.getPoints().size() + "P]")
                                        .collect(Collectors.joining(",", prefix, ")"));
                }

                public boolean canJoin(JoinedWay other) {
                        return getFirstPoint() == other.getFirstPoint() || getFirstPoint() == other.getLastPoint()
                                        || getLastPoint() == other.getFirstPoint() || getLastPoint() == other.getLastPoint();
                }

                public boolean buildsRingWith(JoinedWay other) {
                        return getFirstPoint() == other.getFirstPoint() && getLastPoint() == other.getLastPoint()
                                        || getFirstPoint() == other.getLastPoint() && getLastPoint() == other.getFirstPoint();
                }
               
                /**
                 * Join the other way.
                 *
                 * @param other     the way to be added to this
                 * @throws ExitException if ways cannot be joined
                 */

                private void joinWith(JoinedWay other) {
                        boolean reverseOther = false;
                        int insIdx = -1;
                        int firstOtherIdx = 1;
                       
                        if (this.getFirstPoint() == other.getFirstPoint()) {
                                insIdx = 0;
                                reverseOther = true;
                                firstOtherIdx = 1;
                        } else if (this.getLastPoint() == other.getFirstPoint()) {
                                insIdx = this.getPoints().size();
                                firstOtherIdx = 1;
                        } else if (this.getFirstPoint() == other.getLastPoint()) {
                                insIdx = 0;
                                firstOtherIdx = 0;
                        } else if (this.getLastPoint() == other.getLastPoint()) {
                                insIdx = this.getPoints().size();
                                reverseOther = true;
                                firstOtherIdx = 0;
                        } else {
                                String msg = "Cannot join " + this.getBasicLogInformation() + " with " + other.getBasicLogInformation();
                                log.error(msg);
                                throw new ExitException(msg);
                        }
                       
                        int lastIdx = other.getPoints().size();
                        if (firstOtherIdx == 0) {
                                // the last temp point is already contained in the joined way - do not copy it
                                lastIdx--;
                        }

                        List<Coord> tempCoords = other.getPoints().subList(firstOtherIdx,lastIdx);

                        if (reverseOther) {
                                // the temp coords need to be reversed so copy the list
                                tempCoords = new ArrayList<>(tempCoords);
                                // and reverse it
                                Collections.reverse(tempCoords);
                        }

                        this.getPoints().addAll(insIdx, tempCoords);
                        this.addWay(other);
                }

                /**
                 * Try to find a point that is inside the polygon and store the result.
                 *
                 * @return null or a point that is inside
                 */

                public Coord getPointInside() {
                        if (doPointInsideCalcs) {
                                doPointInsideCalcs = false;
                                Coord test = super.getCofG();
                                if (IsInUtil.isPointInShape(test, getPoints()) == IsInUtil.IN) {
                                        pointInside = test;
                                }
                        }
                        return pointInside;
                }
        }

        protected static class PolygonStatus {
                public final boolean outer;
                public final int index;
                public final JoinedWay polygon;

                public PolygonStatus(boolean outer, int index, JoinedWay polygon) {
                        this.outer = outer;
                        this.index = index;
                        this.polygon = polygon;
                }
               
                public String toString() {
                        return polygon + "_" + outer;
                }
        }

        /**
         * Helper class to bundle objects related to a list of polygons
         *
         */

        protected class Partition {
                public boolean innerEqualsOuter;

                /** list of polygons with a fixed order */
                final List<JoinedWay> polygons;

                final List<BitSet> containsMatrix;
                // Various BitSets which relate to the content of field polygons  
                /** unfinishedPolygons marks which polygons are not yet processed */
                public final BitSet unfinishedPolygons;

                // reporting: BitSets which polygons belong to the outer and to the inner role
                final BitSet innerPolygons;
                final BitSet taggedInnerPolygons;
                final BitSet outerPolygons;
                final BitSet taggedOuterPolygons;
                final BitSet nestedOuterPolygons;
                final BitSet nestedInnerPolygons;
                final BitSet outmostInnerPolygons;
               
                public Partition(List<JoinedWay> list) {
                        this.polygons = Collections.unmodifiableList(list);
                        innerPolygons = new BitSet(list.size());
                        taggedInnerPolygons = new BitSet(list.size());
                        outerPolygons = new BitSet(list.size());
                        taggedOuterPolygons = new BitSet(list.size());
                        analyseRelationRoles();
                        // unfinishedPolygons marks which polygons are not yet processed
                        unfinishedPolygons = new BitSet(list.size());
                        unfinishedPolygons.set(0, list.size());
                        // check which polygons lie inside which other polygon
                        containsMatrix = createContainsMatrix(list);
                        nestedOuterPolygons = new BitSet(list.size());
                        nestedInnerPolygons = new BitSet(list.size());
                        outmostInnerPolygons = new BitSet(list.size());
                       
                        // handle special case produced by partitioning
                        // we may see two identical rectangles for inner and outer
                        if (polygons.size() == 2 && polygons.get(0).getPoints().size() == 5 && polygons.get(1).getPoints().size() == 5 ) {
                                JoinedWay p0 = polygons.get(0);
                                JoinedWay p1 = polygons.get(1);
                                int x = IsInUtil.isLineInShape(p0.getPoints(), p1.getPoints(), p0.getArea());
                                if (x == IsInUtil.ON) {
                                        innerEqualsOuter = true;
                                }
                        }

                }
               
                public void markFinished(PolygonStatus currentPolygon) {
                        unfinishedPolygons.clear(currentPolygon.index);
                }

                /**
                 * Creates a matrix which polygon contains which polygon. A polygon does not
                 * contain itself.
                 * @return
                 */

                private List<BitSet> createContainsMatrix(List<JoinedWay> polygons) {
                        List<BitSet> matrix = new ArrayList<>();
                        for (int i = 0; i < polygons.size(); i++) {
                                matrix.add(new BitSet());
                        }

                        long t1 = System.currentTimeMillis();

                        if (log.isDebugEnabled())
                                log.debug("createContainsMatrix listSize:", polygons.size());

                        // use this matrix to check which matrix element has been
                        // calculated
                        ArrayList<BitSet> finishedMatrix = new ArrayList<>(polygons.size());

                        for (int i = 0; i < polygons.size(); i++) {
                                BitSet matrixRow = new BitSet();
                                // a polygon does not contain itself
                                matrixRow.set(i);
                                finishedMatrix.add(matrixRow);
                        }
                       
                        for (int rowIndex = 0; rowIndex < polygons.size(); rowIndex++) {
                                JoinedWay potentialOuterPolygon = polygons.get(rowIndex);
                                BitSet containsColumns = matrix.get(rowIndex);
                                BitSet finishedCol = finishedMatrix.get(rowIndex);

                                // get all non calculated columns of the matrix
                                for (int colIndex = finishedCol.nextClearBit(0); colIndex >= 0
                                                && colIndex < polygons.size(); colIndex = finishedCol
                                                .nextClearBit(colIndex + 1)) {

                                        JoinedWay innerPolygon = polygons.get(colIndex);

                                        if (potentialOuterPolygon.getBounds().intersects(innerPolygon.getBounds())) {
                                                boolean contains = calcContains(potentialOuterPolygon, innerPolygon);
                                                if (contains) {
                                                        containsColumns.set(colIndex);

                                                        // we also know that the inner polygon does not contain the
                                                        // outer polygon
                                                        // so we can set the finished bit for this matrix
                                                        // element
                                                        finishedMatrix.get(colIndex).set(rowIndex);

                                                        // additionally we know that the outer polygon contains all
                                                        // polygons that are contained by the inner polygon
                                                        containsColumns.or(matrix.get(colIndex));
                                                        finishedCol.or(containsColumns);
                                                }
                                        } else {
                                                // both polygons do not intersect
                                                // we can flag both matrix elements as finished
                                                finishedMatrix.get(colIndex).set(rowIndex);
                                                finishedMatrix.get(rowIndex).set(colIndex);
                                        }
                                        // this matrix element is calculated now
                                        finishedCol.set(colIndex);
                                }
                        }

                        if (log.isDebugEnabled()) {
                                long t2 = System.currentTimeMillis();
                                log.debug("createMatrix for", polygons.size(), "polygons took", (t2 - t1), "ms");

                                log.debug("Containsmatrix:");
                                int i = 0;
                                boolean noContained = true;
                                for (BitSet b : matrix) {
                                        if (!b.isEmpty()) {
                                                log.debug(i, "contains", b);
                                                noContained = false;
                                        }
                                        i++;
                                }
                                if (noContained) {
                                        log.debug("Matrix is empty");
                                }
                        }
                        return matrix;
                }

               
                /**
                 *
                 * @return
                 */

                private BitSet getOutmostRingsAndMatchWithRoles() {
                        BitSet outmostPolygons;
                        boolean outmostInnerFound;
                        do {
                                outmostInnerFound = false;
                                outmostPolygons = findOutmostPolygons(unfinishedPolygons);

                                if (outmostPolygons.intersects(taggedInnerPolygons)) {
                                        // found outmost ring(s) with role inner
                                        outmostInnerPolygons.or(outmostPolygons);
                                        outmostInnerPolygons.and(taggedInnerPolygons);

                                        if (log.isDebugEnabled())
                                                log.debug("wrong inner polygons: " + outmostInnerPolygons);
                                        // do not process polygons tagged with role=inner but which are
                                        // not contained by any other polygon
                                        unfinishedPolygons.andNot(outmostInnerPolygons);
                                        outmostPolygons.andNot(outmostInnerPolygons);
                                        outmostInnerFound = true;
                                }
                        } while (outmostInnerFound);
                        return outmostPolygons;
                }

                /**
                 * Analyse roles in ways and fill corresponding sets.
                 */

                private void analyseRelationRoles() {
                        for (int i = 0; i < polygons.size(); i++) {
                                JoinedWay jw = polygons.get(i);
                                if (jw.intRole == INT_ROLE_INNER) {
                                        innerPolygons.set(i);
                                        taggedInnerPolygons.set(i);
                                } else if (jw.intRole == INT_ROLE_OUTER) {
                                        outerPolygons.set(i);
                                        taggedOuterPolygons.set(i);
                                } else {
                                        // unknown role => it could be both
                                        innerPolygons.set(i);
                                        outerPolygons.set(i);
                                }
                        }
                }

                /**
                 * Report problems which are probably caused by OSM data errors or missing/incomplete data.  
                 */

                public void reportProblems() {
                        if (outmostInnerPolygons.cardinality() + unfinishedPolygons.cardinality()
                                        + nestedOuterPolygons.cardinality() + nestedInnerPolygons.cardinality() >= 1) {
                                log.warn("Multipolygon", toBrowseURL(), toTagString(), "contains errors.");

                                BitSet outerUnusedPolys = new BitSet();
                                outerUnusedPolys.or(unfinishedPolygons);
                                outerUnusedPolys.or(outmostInnerPolygons);
                                outerUnusedPolys.or(nestedOuterPolygons);
                                outerUnusedPolys.or(nestedInnerPolygons);
                                outerUnusedPolys.or(unfinishedPolygons);
                                // use only the outer polygons
                                outerUnusedPolys.and(outerPolygons);
                                for (JoinedWay w : bitsetToList(outerUnusedPolys)) {
                                        //TODO: How do we get here?
                                        outerWaysForLineTagging.addAll(w.getOriginalWays());
                                }

                                runOutmostInnerPolygonCheck(polygons, outmostInnerPolygons);
                                runNestedOuterPolygonCheck(polygons, nestedOuterPolygons);
                                runNestedInnerPolygonCheck(polygons, nestedInnerPolygons);
                                runWrongInnerPolygonCheck(polygons, unfinishedPolygons, innerPolygons);

                                // we have at least one polygon that could not be processed
                                // Probably we have intersecting or overlapping polygons
                                // one possible reason is if the relation overlaps the tile
                                // bounds
                                // => issue a warning
                                List<JoinedWay> lostWays = bitsetToList(unfinishedPolygons);
                                for (JoinedWay w : lostWays) {
                                        log.warn("Polygon", w, "is not processed due to an unknown reason.");
                                        logWayURLs(Level.WARNING, "-", w);
                                }
                        }
                }

                private List<JoinedWay> bitsetToList(BitSet selection) {
                        return selection.stream().mapToObj(polygons::get).collect(Collectors.toList());
                }

                private void runNestedOuterPolygonCheck(List<JoinedWay> polygons, BitSet nestedOuterPolygons) {
                        // just print out warnings
                        // the check has been done before
                        nestedOuterPolygons.stream().forEach(idx ->  {
                                JoinedWay outerWay = polygons.get(idx);
                                log.warn("Polygon",     outerWay, "carries role outer but lies inside an outer polygon. Potentially its role should be inner.");
                                logFakeWayDetails(Level.WARNING, outerWay);
                        });
                }
               
                private void runNestedInnerPolygonCheck(List<JoinedWay> polygons, BitSet nestedInnerPolygons) {
                        // just print out warnings
                        // the check has been done before
                        nestedInnerPolygons.stream().forEach(idx -> {
                                JoinedWay innerWay = polygons.get(idx);
                                log.warn("Polygon",     innerWay, "carries role", getRole(innerWay), "but lies inside an inner polygon. Potentially its role should be outer.");
                                logFakeWayDetails(Level.WARNING, innerWay);
                        });
                }      
               
                private void runOutmostInnerPolygonCheck(List<JoinedWay> polygons, BitSet outmostInnerPolygons) {
                        // just print out warnings
                        // the check has been done before
                        outmostInnerPolygons.stream().forEach(idx -> {
                                JoinedWay innerWay = polygons.get(idx);
                                log.warn("Polygon",     innerWay, "carries role", getRole(innerWay), "but is not inside any other polygon. Potentially it does not belong to this multipolygon.");
                                logFakeWayDetails(Level.WARNING, innerWay);
                        });
                }

                private void runWrongInnerPolygonCheck(List<JoinedWay> polygons, BitSet unfinishedPolygons, BitSet innerPolygons) {
                        // find all unfinished inner polygons that are not contained by any
                        BitSet wrongInnerPolygons = findOutmostPolygons(unfinishedPolygons, innerPolygons);
                        if (log.isDebugEnabled()) {
                                log.debug("unfinished", unfinishedPolygons);
                                log.debug(ROLE_INNER, innerPolygons);
                                // other polygon
                                log.debug("wrong", wrongInnerPolygons);
                        }
                        if (!wrongInnerPolygons.isEmpty()) {
                                // we have an inner polygon that is not contained by any outer polygon
                                // check if
                                wrongInnerPolygons.stream().forEach(wiIndex -> {
                                        BitSet containedPolygons = new BitSet();
                                        containedPolygons.or(unfinishedPolygons);
                                        containedPolygons.and(containsMatrix.get(wiIndex));

                                        JoinedWay innerWay = polygons.get(wiIndex);
                                        if (containedPolygons.isEmpty()) {
                                                log.warn("Polygon",     innerWay, "carries role", getRole(innerWay),
                                                        "but is not inside any outer polygon. Potentially it does not belong to this multipolygon.");
                                                logFakeWayDetails(Level.WARNING, innerWay);
                                        } else {
                                                log.warn("Polygon",     innerWay, "carries role", getRole(innerWay),
                                                        "but is not inside any outer polygon. Potentially the roles are interchanged with the following",
                                                        (containedPolygons.cardinality() > 1 ? "ways" : "way"), ".");
                                                containedPolygons.stream().forEach(wrIndex -> {
                                                        logWayURLs(Level.WARNING, "-", polygons.get(wrIndex));
                                                        unfinishedPolygons.set(wrIndex);
                                                        wrongInnerPolygons.set(wrIndex);
                                                });
                                                logFakeWayDetails(Level.WARNING, innerWay);
                                        }

                                        unfinishedPolygons.clear(wiIndex);
                                        wrongInnerPolygons.clear(wiIndex);
                                });
                        }
                }

                /**
                 * Checks if the polygon with polygonIndex1 contains the polygon with polygonIndex2.
                 *
                 * @return true if polygon(polygonIndex1) contains polygon(polygonIndex2)
                 */

                private boolean contains(int polygonIndex1, int polygonIndex2) {
                        return containsMatrix.get(polygonIndex1).get(polygonIndex2);
                }

                /**
                 * Find all polygons that are not contained by any other polygon.
                 *
                 * @param candidates
                 *            all polygons that should be checked
                 * @param roleFilter
                 *            an additional filter
                 * @return all polygon indexes that are not contained by any other polygon
                 */

                private BitSet findOutmostPolygons(BitSet candidates, BitSet roleFilter) {
                        BitSet realCandidates = ((BitSet) candidates.clone());
                        realCandidates.and(roleFilter);
                        return findOutmostPolygons(realCandidates);
                }

                /**
                 * Finds all polygons that are not contained by any other polygons and that
                 * match the given role. All polygons with index given by
                 * <var>candidates</var> are tested.
                 *
                 * @param candidates indexes of the polygons that should be used
                 * @return set of indexes of all outermost polygons
                 */

                private BitSet findOutmostPolygons(BitSet candidates) {
                        BitSet outmostPolygons = new BitSet();

                        // go through all candidates and check if they are contained by any
                        // other candidate
                        candidates.stream().forEach(candidateIndex -> {
                                // check if the candidateIndex polygon is not contained by any
                                // other candidate polygon
                                boolean isOutmost = candidates.stream()
                                                .noneMatch(otherCandidateIndex -> contains(otherCandidateIndex, candidateIndex));
                                if (isOutmost) {
                                        // this is an outermost polygon
                                        // put it to the bitset
                                        outmostPolygons.set(candidateIndex);
                                }
                        });

                        return outmostPolygons;
                }

                public List<PolygonStatus> getPolygonStatus(PolygonStatus currentPolygon) {
                        ArrayList<PolygonStatus> polygonStatusList = new ArrayList<>();
                        BitSet outmostPolygons;
                        final String defaultRole;
                        if (currentPolygon == null) {
                                outmostPolygons = getOutmostRingsAndMatchWithRoles();
                                defaultRole = ROLE_OUTER;
                        } else {
                                outmostPolygons = checkRoleAgainstGeometry(currentPolygon);
                                defaultRole = currentPolygon.outer ? ROLE_INNER : ROLE_OUTER;
                        }
                        outmostPolygons.stream().forEach(polyIndex -> {
                                // polyIndex is the polygon that is not contained by any other
                                // polygon
                                JoinedWay polygon = polygons.get(polyIndex);
                                String role = getRole(polygon);
                                // if the role is not explicitly set use the default role
                                if (role == null || "".equals(role)) {
                                        role = defaultRole;
                                }
                                polygonStatusList.add(new PolygonStatus(ROLE_OUTER.equals(role), polyIndex, polygon));
                        });
                       
                        // sort by role and then by number of points, this improves performance
                        // in the routines which add the polygons to areas
                        if (polygonStatusList.size() > 2) {
                                polygonStatusList.sort((o1, o2) -> {
                                        if (o1.outer != o2.outer)
                                                return (o1.outer) ? -1 : 1;
                                        return o1.polygon.getPoints().size() - o2.polygon.getPoints().size();
                                });
                        }
                        return polygonStatusList;
                }

                /**
                 * Check the roles of polygons against the actual findings in containsMatrix. Not sure what this does so far.
                 * @param currentPolygon the current polygon
                 * @return set of polygon indexes which are considered as holes of the current polygon  
                 */

                public BitSet checkRoleAgainstGeometry(PolygonStatus currentPolygon) {
                        BitSet polygonContains = new BitSet();
                        polygonContains.or(containsMatrix.get(currentPolygon.index));
                        // use only polygon that are contained by the polygon
                        polygonContains.and(unfinishedPolygons);
                        // polygonContains is the intersection of the unfinished and
                        // the contained polygons

                        // get the holes
                        // these are all polygons that are in the current polygon
                        // and that are not contained by any other polygon
                        boolean holesOk;
                        BitSet holeIndexes;
                        do {
                                holeIndexes = findOutmostPolygons(polygonContains);
                                holesOk = true;

                                if (currentPolygon.outer) {
                                        // for role=outer only role=inner is allowed
                                        if (holeIndexes.intersects(taggedOuterPolygons)) {
                                                BitSet addOuterNestedPolygons = new BitSet();
                                                addOuterNestedPolygons.or(holeIndexes);
                                                addOuterNestedPolygons.and(taggedOuterPolygons);
                                                nestedOuterPolygons.or(addOuterNestedPolygons);
                                                holeIndexes.andNot(addOuterNestedPolygons);
                                                // do not process them
                                                unfinishedPolygons.andNot(addOuterNestedPolygons);
                                                polygonContains.andNot(addOuterNestedPolygons);
                                               
                                                // recalculate the holes again to get all inner polygons
                                                // in the nested outer polygons
                                                holesOk = false;
                                        }
                                } else {
                                        // for role=inner both role=inner and role=outer is supported
                                        // although inner in inner is not officially allowed
                                        if (holeIndexes.intersects(taggedInnerPolygons)) {
                                                // process inner in inner but issue a warning later
                                                BitSet addInnerNestedPolygons = new BitSet();
                                                addInnerNestedPolygons.or(holeIndexes);
                                                addInnerNestedPolygons.and(taggedInnerPolygons);
                                                nestedInnerPolygons.or(addInnerNestedPolygons);
                                        }
                                }
                        } while (!holesOk);
                        return holeIndexes;
                }
        }
       
        private void divideLargest(List<JoinedWay> partition, List<List<JoinedWay>> partitions, int depth) {
                if (partition.isEmpty())
                        return;
                // probably complex polygons with crossing /self intersecting ways will be problematic
                if (depth >= 10 || partition.size() < 2 || tagIsLikeYes("expect-self-intersection")) {
                        partitions.add(partition);
                        return;
                }
                JoinedWay mostComplex = partition.get(0);
                for (JoinedWay jw : partition) {
                        if (mostComplex.getPoints().size() < jw.getPoints().size())
                                mostComplex = jw;
                }
                if (mostComplex.getPoints().size() > 2000) {
                        uk.me.parabola.imgfmt.app.Area fullArea = calcBounds(partition);
                        uk.me.parabola.imgfmt.app.Area[] areas;
                        final int niceSplitShift = 11;
                        if (fullArea.getHeight() > fullArea.getWidth())
                                areas = fullArea.split(1, 2, niceSplitShift);
                        else
                                areas = fullArea.split(2, 1, niceSplitShift);
                        // code to calculate dividingLine taken from MapSplitter
                        if (areas != null && areas.length == 2) {
                                int dividingLine = 0;
                                boolean isLongitude = false;
                                boolean commonLine = true;
                                if (areas[0].getMaxLat() == areas[1].getMinLat()) {
                                        dividingLine = areas[0].getMaxLat();
                                } else if (areas[0].getMaxLong() == areas[1].getMinLong()) {
                                        dividingLine = areas[0].getMaxLong();
                                        isLongitude = true;
                                } else {
                                        commonLine = false;
                                        log.error("Split into 2 expects shared edge between the areas");
                                }
                                if (commonLine) {
                                        List<JoinedWay> dividedLess = new ArrayList<>();
                                        List<JoinedWay> dividedMore = new ArrayList<>();
                                        for (int i = 0; i < partition.size(); i++) {
                                                JoinedWay jw = partition.get(i);
                                               
                                                List<List<Coord>> lessList = new ArrayList<>();
                                                List<List<Coord>> moreList = new ArrayList<>();
                                                ShapeSplitter.splitShape(jw.getPoints(), dividingLine << Coord.DELTA_SHIFT, isLongitude,
                                                                lessList, moreList, commonCoordMap);
                                               
                                                lessList.forEach(part -> dividedLess.add(new JoinedWay(jw, part)));
                                                moreList.forEach(part -> dividedMore.add(new JoinedWay(jw, part)));
                                        }
                                        divideLargest(dividedLess, partitions, depth + 1);
                                        divideLargest(dividedMore, partitions, depth + 1);
                                        return;
                                }
                        }
                }
                partitions.add(partition);
        }


        public Way getLargestOuterRing() {
                return largestOuterPolygon;
        }
       
        public List<JoinedWay> getRings() {
                if (polygons == null) {
                        polygons = buildRings();
                        cleanup();
                }
                return polygons;
        }


        public void setNoRecalc(boolean b) {
                this.noRecalc = b;
        }

        public boolean isNoRecalc() {
                return noRecalc;
        }


}