Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2014 Gerd Petermann
 *
 * 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.util;

import java.awt.Shape;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;

public class ShapeSplitter {
        private static final Logger log = Logger.getLogger(ShapeSplitter.class);
        private static final int LEFT = 0;
        private static final int TOP = 1;
        private static final int RIGHT = 2;
        private static final int BOTTOM= 3;

       
        /**
         * Clip a given shape with a given rectangle.
         * @param shape the subject shape to clip
         * @param clippingRect the clipping rectangle
         * @return the intersection of the shape and the rectangle
         * or null if they don't intersect.
         * The intersection may contain dangling edges.
         */

        public static Path2D.Double clipShape (Shape shape, Rectangle2D clippingRect) {
                double minX = Double.POSITIVE_INFINITY,minY = Double.POSITIVE_INFINITY,
                                maxX = Double.NEGATIVE_INFINITY,maxY = Double.NEGATIVE_INFINITY;
                PathIterator pit = shape.getPathIterator(null);
                double[] points = new double[512];
                int num = 0;
                Path2D.Double result = null;
                double[] res = new double[6];
                while (!pit.isDone()) {
                        int type = pit.currentSegment(res);
                        double x = res[0];
                        double y = res[1];
                        if (x < minX) minX = x;
                        if (x > maxX) maxX = x;
                        if (y < minY) minY = y;
                        if (y > maxY) maxY = y;
                        switch (type) {
                        case PathIterator.SEG_LINETO:
                        case PathIterator.SEG_MOVETO:
                                if (num  + 2 >= points.length) {
                                        points = Arrays.copyOf(points, points.length * 2);
                                }
                                points[num++] = x;
                                points[num++] = y;
                                break;
                        case PathIterator.SEG_CLOSE:
                                Path2D.Double segment = null;
                                if (!clippingRect.contains(minX, minY) || !clippingRect.contains(maxX,maxY)) {
                                        Rectangle2D.Double bbox = new Rectangle2D.Double(minX,minY,maxX-minX,maxY-minY);
                                        segment = clipSinglePathWithSutherlandHodgman (points, num, clippingRect, bbox);
                                } else
                                        segment = pointsToPath2D(points, num);
                                if (segment != null){
                                        if (result == null)
                                                result = segment;
                                        else
                                                result.append(segment, false);
                                }
                                num = 0;
                                minX = minY = Double.POSITIVE_INFINITY;
                                maxX = maxY = Double.NEGATIVE_INFINITY;
                                break;
                        default:
                                log.error("Unsupported path iterator type " + type
                                                + ". This is an mkgmap error.");
                        }
       
                        pit.next();
                }
                return result;
        }

        /**
         * Convert a list of longitude+latitude values to a Path2D.Double
         * @param points the pairs
         * @return the path or null if the path describes a point or line.
         */

        public static Path2D.Double pointsToPath2D(double[] points, int num) {
                if (num < 2)
                        return null;
                if (points[0] == points[num-2] && points[1] == points[num-1])
                        num -= 2;
                if (num < 6)
                        return null;
                Path2D.Double path = new Path2D.Double(Path2D.WIND_NON_ZERO, num / 2 + 2);
                double lastX = points[0], lastY = points[1];
                path.moveTo(lastX, lastY);
                int numOut = 1;
                for (int i = 2; i < num; ){
                        double x = points[i++], y = points[i++];
                        if (x != lastX || y != lastY){
                                path.lineTo(x,y);
                                lastX = x; lastY = y;
                                ++numOut;
                        }
                }
                if (numOut < 3)
                        return null;
                path.closePath();
                return path;
        }

        //    The Sutherland�Hodgman algorithm Pseudo-Code:    
        //        List outputList = subjectPolygon;
        //        for (Edge clipEdge in clipPolygon) do
        //           List inputList = outputList;
        //           outputList.clear();
        //           Point S = inputList.last;
        //           for (Point E in inputList) do
        //              if (E inside clipEdge) then
        //                 if (S not inside clipEdge) then
        //                    outputList.add(ComputeIntersection(S,E,clipEdge));
        //                 end if
        //                 outputList.add(E);
        //              else if (S inside clipEdge) then
        //                 outputList.add(ComputeIntersection(S,E,clipEdge));
        //              end if
        //              S = E;
        //           done
        //        done

        /**
         * Clip a single path with a given rectangle using the Sutherland-Hodgman algorithm. This is much faster compared to
         * the area.intersect method, but may create dangling edges.
         * @param points        a list of longitude+latitude pairs  
         * @param num the nnumber of valid values in points
         * @param clippingRect the clipping rectangle
         * @param bbox the bounding box of the path
         * @return the clipped path as a Path2D.Double or null if the result is empty  
         */

        public static Path2D.Double clipSinglePathWithSutherlandHodgman (double[] points, int num, Rectangle2D clippingRect, Rectangle2D.Double bbox) {
                if (num <= 2 || !bbox.intersects(clippingRect)) {
                        return null;
                }
               
                int countVals = num;
                if (points[0] == points[num-2] && points[1] == points[num-1]){
                        countVals -= 2;
                }
                double[] outputList = points;
                double[] input;
               
                double leftX = clippingRect.getMinX();
                double rightX = clippingRect.getMaxX();
                double lowerY = clippingRect.getMinY();
                double upperY = clippingRect.getMaxY();
                boolean eIsIn = false, sIsIn = false;
                for (int side = LEFT; side <= BOTTOM; side ++){
                        if (countVals < 6)
                                return null; // ignore point or line
                       
                        boolean skipTestForThisSide;
                        switch(side){
                        case LEFT: skipTestForThisSide = (bbox.getMinX() >= leftX); break;
                        case TOP: skipTestForThisSide = (bbox.getMaxY()  < upperY); break;
                        case RIGHT: skipTestForThisSide = (bbox.getMaxX()  < rightX); break;
                        default: skipTestForThisSide = (bbox.getMinY() >= lowerY);
                        }
                        if (skipTestForThisSide)
                                continue;
                       
                        input = outputList;
                        outputList = new double[countVals + 16];
                        double sLon = 0,sLat = 0;
                        double pLon = 0,pLat = 0; // intersection
                        int posIn = countVals - 2;
                        int posOut = 0;
                        for (int i = 0; i < countVals+2; i+=2){
                                if (posIn >=  countVals)
                                        posIn = 0;
                                double eLon = input[posIn++];
                                double eLat = input[posIn++];
                                switch (side){
                                case LEFT: eIsIn =  (eLon >= leftX); break;
                                case TOP: eIsIn =  (eLat < upperY); break;
                                case RIGHT: eIsIn =  (eLon < rightX); break;
                                default: eIsIn =  (eLat >= lowerY);
                                }
                                if (i > 0){
                                        if (eIsIn != sIsIn){
                                                // compute intersection
                                                double slope;
                                                if (eLon != sLon)
                                                        slope = (eLat - sLat) / (eLon-sLon);
                                                else slope = 1;
       
                                                switch (side){
                                                case LEFT:
                                                        pLon = leftX;
                                                        pLat = slope *(leftX-sLon) + sLat;
                                                        break;
                                                case RIGHT:
                                                        pLon = rightX;
                                                        pLat = slope *(rightX-sLon) + sLat;
                                                        break;
       
                                                case TOP:
                                                        if (eLon != sLon)
                                                                pLon =  sLon + (upperY - sLat) / slope;
                                                        else
                                                                pLon =  sLon;
                                                        pLat = upperY;
                                                        break;
                                                default: // BOTTOM
                                                        if (eLon != sLon)
                                                                pLon =  sLon + (lowerY - sLat) / slope;
                                                        else
                                                                pLon =  sLon;
                                                        pLat = lowerY;
                                                        break;
       
                                                }
                                        }
                                        int toAdd = 0;
                                        if (eIsIn){
                                                if (!sIsIn){
                                                        toAdd += 2;
                                                }
                                                toAdd += 2;
                                        }
                                        else {
                                                if (sIsIn){
                                                        toAdd += 2;
                                                }
                                        }
                                        if (posOut + toAdd >= outputList.length) {
                                                // unlikely
                                                outputList = Arrays.copyOf(outputList, outputList.length * 2);
                                        }
                                        if (eIsIn){
                                                if (!sIsIn){
                                                        outputList[posOut++] = pLon;
                                                        outputList[posOut++] = pLat;
                                                }
                                                outputList[posOut++] = eLon;
                                                outputList[posOut++] = eLat;
                                        }
                                        else {
                                                if (sIsIn){
                                                        outputList[posOut++] = pLon;
                                                        outputList[posOut++] = pLat;
                                                }
                                        }
                                }
                                // S = E
                                sLon = eLon; sLat = eLat;
                                sIsIn = eIsIn;
                        }
                        countVals = posOut;
                }
                return pointsToPath2D(outputList, countVals);
        }

// Dec16/Jan17. Ticker Berkin. New implementation for splitting shapes and clipping

        private boolean detectedProblems;
        private List<MergeCloseHelper> newLess, newMore;
        private String gpxDirectory;
        private long fullArea;

        private List<LoopEndPoint> endPoints; // for the side we are working on
        private List<List<Coord>> origList; // ditto

        private boolean multSamePoint; // .sort(comparator) might set this

        // following are for processAwkward...
        private LoopEndPoint forwardLine, backwardLine;
        private int directionBalance;
        private List<LoopEndPoint> dLoops, shapes, holes;
        private boolean droppedEmpty;

        private void logMsg(Object ... olist) {
                detectedProblems = true;
                log.warn(olist);
        }

        /**
         * Service routine for processLineList. Processes a nested list of holes within a shape or
         * list of shapes within a hole.
         *
         * Recurses to check for and handle the opposite of what has been called to process.
         *
         * @param startInx starting point in list.
         * @param endInx index of highPoint of enclosing hole/shape.
         * @param addHolesToThis if not null, then called from a shape and subtract holes from it
         * otherwise new shapes within a hole.
         */

        private void doLines(int startInx, int endInx, MergeCloseHelper addHolesToThis) {
                int inx = startInx;
                final boolean calledFromHole = addHolesToThis == null;
                while (inx < endInx) {
                        LoopEndPoint lowElem = endPoints.get(inx);
                        int otherEndInx = lowElem.otherEnd.ownIndex;
                        MergeCloseHelper thisLine = lowElem.theLoop;
                        if (lowElem.lowEnd != 1) {
                                logMsg("Wrong end of shape/hole encountered", inx, startInx, endInx, calledFromHole);
                                return;
                        }
                        doLines(inx+1, otherEndInx, calledFromHole ? thisLine : null);
                        inx = otherEndInx + 1;
                        if (calledFromHole) // handling list of shapes
                                thisLine.closeAppend(true);
                        else // handling list of holes
                                addHolesToThis.addHole(thisLine);
                }
        } // doLines

        /**
         * Service routine for splitShape. Takes list of lines and appends distinct shapes
         * @param lineInfo list of lines that start and end on the dividing line (or orig startPoint)
         * @param origList list of shapes to which we append new shapes formed from above
         */

        private void processLineList(List<MergeCloseHelper> lineInfo, List<List<Coord>> origList) {
                this.origList = origList;
                if (origList == null) // never wanted this side
                        return;
                MergeCloseHelper firstLine = lineInfo.get(0);
                if (lineInfo.size() == 1) { // single shape that never crossed line
                        if (!firstLine.points.isEmpty()) // all on this side
                                firstLine.closeAppend(false);
                        return;
                }
                // look at last item in list of lines
                MergeCloseHelper lastLine = lineInfo.get(lineInfo.size()-1);
                if (lastLine.points.isEmpty()) // will be empty if started on other side
                        lineInfo.remove(lineInfo.size()-1);
                else { // ended up on this side and must have crossed the line
                        // so first element is really the end of the last
                        lastLine.combineFirstIntoLast(firstLine);
                        lineInfo.remove(0);
                        firstLine = lineInfo.get(0);
                }
                if (lineInfo.size() == 1) { // simple poly that crossed once and back
                        firstLine.setMoreInfo(0);
                        firstLine.closeAppend(true);
                        return;
                }
                // Above were the simple cases - probably 99% of calls.

                // splitShape has generated a list of lines that start and end on the dividing line.
                // These lines don't cross. Order their end-points by their lowest point on the divider and note which
                // direction they go. The first (and last) point must define a shape. Starting with this
                // shape; the next line up, if it is within this shape, must define a hole and
                // so is added to the list of points for the shape. For the hole, recurse to
                // handle any shapes enclosed. Repeat until we reach the end of the enclosing
                // space.

                final int fullAreaSign = Long.signum(fullArea);
                // check and set any missing directions based on signs of full/area
                boolean someDirectionsNotSet = false;
                int areaDirection = 0;
                // and build list of all endpoints
                endPoints = new ArrayList<>(lineInfo.size()*2);
                for (MergeCloseHelper thisLine : lineInfo) {
                        thisLine.setMoreInfo(fullAreaSign);
                        if (thisLine.direction == 0)
                                someDirectionsNotSet = true;
                        else if (thisLine.areaToLine != 0) {
                                int tmpAreaDirection = thisLine.direction * Long.signum(thisLine.areaToLine);
                                if (areaDirection == 0)
                                        areaDirection = tmpAreaDirection;
                                else if (areaDirection != tmpAreaDirection)
                                        logMsg("Direction/Area conflict", fullAreaSign, areaDirection, tmpAreaDirection);
                        }
                        // create endPoint elements
                        LoopEndPoint lep1 = new LoopEndPoint(thisLine.lowPoint,  +1, thisLine);
                        LoopEndPoint lep2 = new LoopEndPoint(thisLine.highPoint, -1, thisLine);
                        lep1.otherEnd = lep2;
                        lep2.otherEnd = lep1;
                        endPoints.add(lep1);
                        endPoints.add(lep2);
                }
                if (someDirectionsNotSet) {
                        if (areaDirection == 0)
                                logMsg("Can't deduce direction/Area mapping", fullAreaSign);
                        else
                                for (MergeCloseHelper thisLine : lineInfo)
                                        if (thisLine.direction == 0)
                                                thisLine.direction = areaDirection * Long.signum(thisLine.areaToLine);
                }

                Comparator<LoopEndPoint> comparator = new EndPointComparator();
                multSamePoint = false;
                endPoints.sort(comparator);
                if (multSamePoint)
                        processAwkward(comparator);
//              if (log.isDebugEnabled()) { // can be useful to have raw loop data before shape/hole processing
//                      int fInx = 0;
//                      for (MergeCloseHelper thisLine : lineInfo) {
//                              ++fInx;
//                              uk.me.parabola.util.GpxCreator.createGpx(gpxDirectory + (lineInfo == newLess ? "N" : "P") + fInx, thisLine.points);
//                      }
//              }

                // set ownIndex in each element after sorting
                int ownIndex = 0;
                for (LoopEndPoint endPoint : endPoints) {
                        endPoint.ownIndex = ownIndex;
                        ++ownIndex;
                }
                doLines(0, endPoints.size(), null);
                if (detectedProblems && log.isDebugEnabled())
                        for (LoopEndPoint endPoint : endPoints)
                                log.warn("ep", endPoint.ownIndex, endPoint.thePoint, endPoint.lowEnd, System.identityHashCode(endPoint),
                                                 System.identityHashCode(endPoint.otherEnd), endPoint.theLoop);
        } // processLineList

        private void processAwkward(Comparator<LoopEndPoint> comparator) {
                // Where a loop has lowPoint==highPoint, let us call it a "balloon", otherwise call it a Dloop.
                // Awkward cases are:
                //  Dloops with same low/high/area, For this to be true they must follow the same path (or intersect)
                //  Multiple hole balloons from the same point
                //  Balloon(s) that share the same point as dLoops
                boolean haveBalloons = false;

                // Duplicate dLoops in same direction can be removed / opposite direction cancel each other out.
                // Do this before Balloon processing as dLoop removal can make some problems go away.
                List<LoopEndPoint> newList = new ArrayList<>(endPoints.size());
                LoopEndPoint prevElem = null;
                boolean grouping = false;
                for (LoopEndPoint thisElem : endPoints) {
                        if (prevElem != null) {
                                boolean sameAsPrev = comparator.compare(thisElem, prevElem) == 0;
                                // balloons won't break up the list because they will be inside the dLoops
                                // The two ends of the balloon don't compare equal
                                fixDups(newList, prevElem, sameAsPrev, grouping);
                                grouping = sameAsPrev;
                        }
                        prevElem = thisElem;
                        if (thisElem.theLoop.lowPoint == thisElem.theLoop.highPoint)
                                haveBalloons = true;
                }
                fixDups(newList, prevElem, false, grouping);

                if (newList.size() < endPoints.size()) {
                        log.debug("Reduced dLoops from", endPoints.size(), "to", newList.size());
                        endPoints.clear();
                        endPoints.addAll(newList);
                }

                if (!haveBalloons)
                        return;

                // Balloons will be sorted within the dLoops that share the same lowPoint or highPoint,
                // but those that form a shape must be within a hole and those that form a hole must be within
                // a shape and so might need moving.
                // A single dLoop defines a transition and so we can get balloons on the correct side of it.
                // Multiple dLoops might suggest more than 1 place where +ve or -ve balloons can go and
                // this isn't possible to resolve without much more complex analysis of the geometry away from the cut-point.
                // The ordering of multiple +ve balloons doesn't matter - they will become individual shapes.
                // The ordering of multiple -ve balloons does matter - in the wrong order a crossing will be generated
                // at the cut-point - again this isn't possible to solve without analysis of the geometry
                newList = new ArrayList<>(endPoints.size());
                dLoops = new ArrayList<>();
                shapes = new ArrayList<>();
                holes  = new ArrayList<>();
                droppedEmpty = false;
                boolean reordered = false;
                prevElem = null;
                grouping = false;
                for (LoopEndPoint thisElem : endPoints) {
                        if (prevElem != null) {
                                boolean sameAsPrev = thisElem.thePoint == prevElem.thePoint;
                                reordered |= fixOrder(newList, prevElem, sameAsPrev, grouping, comparator);
                                grouping = sameAsPrev;
                        }
                        prevElem = thisElem;
                }
                reordered |= fixOrder(newList, prevElem, false, grouping, comparator);

                if (reordered || droppedEmpty) {
                        log.debug("Reordered endPoints or droppedEmpty", droppedEmpty);
                        endPoints.clear();
                        endPoints.addAll(newList);
                }
        } // processAwkward

        private void fixDups(List<LoopEndPoint> newList, LoopEndPoint prevElem, boolean sameAsPrev, boolean grouping) {
                if (grouping || sameAsPrev) {
                        if (prevElem.lowEnd == 1) {
                                if (!grouping) { // start of a group
                                        forwardLine = null;
                                        backwardLine = null;
                                        directionBalance = 0;
                                }
                                if (prevElem.theLoop.direction > 0) {
                                        forwardLine = prevElem;
                                        ++directionBalance;
                                } else {
                                        backwardLine = prevElem;
                                        --directionBalance;
                                }
                                prevElem.theLoop.removedDloop = true; // mark removed so can remove other end
                        } else { // the high end, just keep the undelete ones
                                if (!prevElem.theLoop.removedDloop)
                                        newList.add(prevElem);
                        }
                }
                if (sameAsPrev)
                        return;
                // flush previous
                if (!grouping) {
                        newList.add(prevElem);
                        return;
                }
                if (prevElem.lowEnd == 1) {
                        if (directionBalance > 0) {
                                newList.add(forwardLine);
                                forwardLine.theLoop.removedDloop = false; // undelete
                        } else if (directionBalance < 0) {
                                newList.add(backwardLine);
                                backwardLine.theLoop.removedDloop = false; // undelete
                        }
                }
        } // fixDups

        private boolean fixOrder(List<LoopEndPoint> newList, LoopEndPoint prevElem, boolean sameAsPrev, boolean grouping,
                                                         Comparator<LoopEndPoint> comparator) {
                if (grouping || sameAsPrev) {
                        if (prevElem.theLoop.lowPoint != prevElem.theLoop.highPoint)
                                dLoops.add(prevElem);
                        else if (prevElem.theLoop.areaOrHole == 1)
                                shapes.add(prevElem);
                        else if (prevElem.theLoop.areaOrHole == -1)
                                holes.add(prevElem);
                        else // zero area, can't tell if shape or hole and could end in wrong place if left
                                droppedEmpty = true;
                }
                if (sameAsPrev)
                        return false;
                // flush previous
                if (!grouping) {
                        newList.add(prevElem);
                        return false;
                }

                if (holes.size() > 2) // NB: each hole/shape has 2 ends at this point
                        log.warn("Cutting", holes.size()/2, "holes at same point might cause self-intersection",
                                         holes.get(0).theLoop.points.get(0).toOSMURL());
                if (dLoops.isEmpty()) {
                        if (shapes.isEmpty()) {
                                newList.addAll(holes);
                                holes.clear();
                                return false;
                        } else if (holes.isEmpty()) {
                                newList.addAll(shapes);
                                shapes.clear();
                                return false;
                        }
                        // single hole and single shape must be nested. Already warned for mult holes
                        if (shapes.size() > 2)
                                log.warn("Cutting hole and ", shapes.size()/2, "shapes at same point is problematic",
                                                 shapes.get(0).theLoop.points.get(0).toOSMURL());
                        // assume nested - have lost original sort which would have been good, so redo:
                        shapes.addAll(holes);
                        holes.clear();
                        shapes.sort(comparator);
                        newList.addAll(shapes);
                        shapes.clear();
                        return true;
                } else {
                        if (shapes.isEmpty() && holes.isEmpty()) {
                                newList.addAll(dLoops);
                                dLoops.clear();
                                return false;
                        }
                }

                // might be able to do a few more limitations based on areas
                if (dLoops.get(0).theLoop.areaOrHole == dLoops.get(0).lowEnd) { // lowEnd of area or highEnd of hole
                        newList.addAll(shapes);
                        newList.add(dLoops.get(0));
                        newList.addAll(holes);
                } else {
                        newList.addAll(holes);
                        newList.add(dLoops.get(0));
                        newList.addAll(shapes);
                }
                dLoops.remove(0);
                if (!dLoops.isEmpty()) {
                        // if 2 dividors hole>area | areae>hole then, as only place for holes is middle, can avoid this warning
                        log.warn("Possible ambiguous balloon allocation at", dLoops.get(0).theLoop.points.get(0).toOSMURL(),
                                         "Dloops:", dLoops.size(), "shapes:", shapes.size()/2, "holes:", holes.size()/2);
                        newList.addAll(dLoops);
                        dLoops.clear();
                }
                shapes.clear();
                holes.clear();
                return true;
        } // fixOrder

        private List<Coord> startLine(List<MergeCloseHelper> lineInfo) {
                MergeCloseHelper thisLine = new MergeCloseHelper();
                lineInfo.add(thisLine);
                return thisLine.points;
        } // startLine

        private void openLine(List<MergeCloseHelper> lineInfo, Coord lineCoord, int lineAlong, long currentArea) {
                MergeCloseHelper thisLine = lineInfo.get(lineInfo.size()-1);
                thisLine.points.add(lineCoord);
                thisLine.firstPoint = lineAlong;
                thisLine.startingArea = currentArea;
        } // openLine

        private List<Coord> closeLine(List<MergeCloseHelper> lineInfo, Coord lineCoord, int lineAlong, long currentArea) {
                MergeCloseHelper thisLine = lineInfo.get(lineInfo.size()-1);
                thisLine.points.add(lineCoord);
                thisLine.lastPoint = lineAlong;
                thisLine.endingArea = currentArea;
                return startLine(lineInfo);
        } // closeLine

        /**
         * Helper class for splitShape. Holds information about line.
         * Sorts array/list of itself according to lowest point on dividing line.
         */

        private class MergeCloseHelper {

                List<Coord> points;
                int firstPoint, lastPoint;
                long startingArea, endingArea; // from runningArea
                int direction;
                int lowPoint, highPoint;
                long areaToLine;
                int areaOrHole; // +1/-1
                boolean removedDloop; // so can remove both ends of the same one

                MergeCloseHelper() {
                        points = new ArrayList<>();
                } // MergeCloseHelper

                public String toString() {
                        return "fp=" + firstPoint + " lp=" + lastPoint + " area=" + areaToLine + " #=" + points.size() + " " +
                                points.get(1).toOSMURL() + " " + points.get(points.size()/2).toOSMURL();
                } // toString

                void setMoreInfo(int fullAreaSign) {
                        this.direction = Integer.signum(lastPoint - firstPoint);
                        if (this.direction > 0) {
                                this.lowPoint = firstPoint;
                                this.highPoint = lastPoint;
                        } else {
                                this.lowPoint = lastPoint;
                                this.highPoint = firstPoint;
                        }
                        this.areaToLine = this.endingArea - this.startingArea; // correct if closed
                        // !!! also correct when close along the line, because would do:
//                      this.areaToLine += (long)(lastPoint + firstPoint) * (dividingLine - dividingLine);
                        this.areaOrHole = fullAreaSign * Long.signum(this.areaToLine);
                } // setMoreInfo

                void combineFirstIntoLast(MergeCloseHelper other) {
                        this.points.addAll(other.points);
                        this.lastPoint = other.lastPoint;
                        this.endingArea = fullArea + other.endingArea;
                } // combineFirstIntoLast

                void addHole(MergeCloseHelper other) {
                        if (other.areaToLine == 0)
                                return; // spike into this area. cf. closeAppend()
                        // shapes must have opposite directions.
                        if (this.direction == 0 && other.direction == 0)
                                logMsg("Direction of shape and hole indeterminate.", "shape:", this, "hole:", other);
                        else if (this.direction != 0 && other.direction != 0 && this.direction == other.direction)
                                logMsg("Direction of shape and hole conflict.", "shape:", this, "hole:", other);
                        else if (this.direction < 0 || other.direction > 0) {
                                this.points.addAll(other.points);
                                if (this.direction == 0)
                                        this.direction = -1;
                        } else { // add at start
                                other.points.addAll(this.points);
                                this.points = other.points;
                                if (this.direction == 0)
                                        this.direction = +1;
                        }
                        this.areaToLine += other.areaToLine;
                } // addHole

                /**
                 * closes a shape and appends to list.
                 *
                 * If the shape starts and ends at the same point on the dividing line then
                 * there is no need to close it. Also check for and chuck a spike, which happens
                 * if there is a single point just across the dividing line and the two intersecting
                 * points ended up being the same or an edge runs back on itself exactly.
                 *
                 * @param onDividingLine if false, shape not cut so don't assume/care much about it
                 */

                void closeAppend(boolean onDividingLine) {
                        final Coord firstCoord = points.get(0);
                        final int lastPointInx = points.size()-1;
                        if (lastPointInx >= 3 &&
                                firstCoord.highPrecEquals(points.get(lastPointInx))) { // by chance, ends up closed
                                // There is no need to close the shape along the line, but am finding, for shapes that never crossed the
                                // dividing line, quite a few that, after splitShapes has rotating the shape by 1, have first and last
                                // points highPrecEquals but they are different objects.
                                // This means that the original first and last were the same object, but the second and last were highPrecEquals!
                                // If left like this, it might be flagged by ShapeMergeFilter.
                                // NB: if no coordPool, likely to be different closing object anyway
                                if (firstCoord != points.get(lastPointInx))
                                        points.set(lastPointInx, firstCoord); // quietly replace with first point
                        } else
                                points.add(firstCoord); // close it
                        if (onDividingLine) { // if not, just one shape untouched by chopping
                                if (this.areaToLine == 0)
                                        return;
                        }
                        origList.add(points);
                } // closeAppend

        } // MergeCloseHelper

        private class LoopEndPoint {

                int thePoint;
                int lowEnd; // +1 if thePoint is lowPoint, -1 otherwise
                MergeCloseHelper theLoop;
                LoopEndPoint otherEnd;
                int ownIndex; // set after sort

                LoopEndPoint(int thePoint, int lowEnd, MergeCloseHelper theLoop) {
                        this.thePoint = thePoint;
                        this.lowEnd = lowEnd;
                        this.theLoop = theLoop;
                } // LoopEndPoint

        } // LoopEndPoint

        private class EndPointComparator implements Comparator<LoopEndPoint> {

                @Override
                public int compare(LoopEndPoint lep1, LoopEndPoint lep2) {
                        int cmp;
                        // sort in ascending thePoint order
                        cmp = lep1.thePoint - lep2.thePoint;
                        if (cmp != 0)
                                return cmp;
                        multSamePoint = true;

                        MergeCloseHelper mch1 = lep1.theLoop;
                        MergeCloseHelper mch2 = lep2.theLoop;
                        if (mch1 == mch2) // same object - get ends in correct order
                                return lep2.lowEnd;

                        if (lep1.lowEnd == 1)
                                if (lep2.lowEnd == 1)
                                        cmp     = mch2.highPoint - mch1.highPoint;
                                else // low == high
                                        return +1;
                        else // high
                                if (lep2.lowEnd == 1) // high == low
                                        return -1;
                                else
                                        cmp     = mch2.lowPoint - mch1.lowPoint;
                        if (cmp != 0)
                                return cmp;

                        // have same start & end (and orientation), widen by area (ie lowEnd down, highEnd up)
                        cmp = Long.compare(Math.abs(mch2.areaToLine), Math.abs(mch1.areaToLine));
                        if (cmp != 0)
                                return cmp * lep1.lowEnd;
                        // for same low/high/area must return equal/0 for duplicate detection and removal
                        return 0;
                } // compare

        } // EndPointComparator

        /**
         * split a shape with a line
         * @param coords the shape. Must be closed.
         * @param dividingLine the line in high precision.
         * @param isLongitude true if above is line of longitude, false if latitude.
         * @param lessList list of shapes to which we append new shapes on lower/left side of line.
         * @param moreList list of shapes to which we append new shapes on upper/right side of line.
         * @param coordPool if not null, hashmap for created coordinates. Will all be on the line.
         */

        public static void splitShape(List<Coord> coords, int dividingLine, boolean isLongitude,
                                                                  List<List<Coord>> lessList, List<List<Coord>> moreList,
                                                                  Long2ObjectOpenHashMap<Coord> coordPool) {
                ShapeSplitter ss = new ShapeSplitter();
                ss.split(coords, dividingLine, isLongitude, lessList, moreList, coordPool);
        } // splitShape

        private void split(List<Coord> coords, int dividingLine, boolean isLongitude,
                                           List<List<Coord>> lessList, List<List<Coord>> moreList,
                                           Long2ObjectOpenHashMap<Coord> coordPool) {
                if (log.isDebugEnabled()) {
                        gpxDirectory = (isLongitude ? "V" : "H") + dividingLine + "_" +
                                (isLongitude ? coords.get(0).getLatitude() : coords.get(0).getLongitude()) + "/";
                }
                formLoops(coords, dividingLine, isLongitude, lessList != null, moreList != null, coordPool);
                processLineList(newLess, lessList);
                processLineList(newMore, moreList);
                if (detectedProblems) {
                        logDiagInfo(coords, lessList, moreList);
                        log.warn(isLongitude ? "Vertical" : "Horizontal", "split", dividingLine, "failed on shape at", coords.get(0).toOSMURL(),
                                          "Possibly a self-intersecting polygon");
                }
        } // split

        private void formLoops(List<Coord> coords, int dividingLine, boolean isLongitude,
                                                   boolean wantLess, boolean wantMore, Long2ObjectOpenHashMap<Coord> coordPool) {
                List<Coord> lessPoly = null, morePoly = null;
                if (wantLess) {
                        newLess = new ArrayList<>();
                        lessPoly = startLine(newLess);
                }
                if (wantMore) {
                        newMore = new ArrayList<>();
                        morePoly = startLine(newMore);
                }
                /**
                 * trailXxx variables are the previous coordinate information.
                 * leadXxx            are for the current coordinate
                 * lineXxx            are the crossing point
                 * where Xxx is Coord : Coord
                 *              Away  : position in plane at right angles to dividing line
                 *              Along : position in same plane as the dividing line
                 *              Rel   : -1/0/+1 depending on relationship of Away to dividing line
                 */

                Coord trailCoord = null;
                int trailAway = 0, trailAlong = 0, trailRel = 0;
                long runningArea = 0;

                for (Coord leadCoord : coords) {
                        final int leadAway  = isLongitude ? leadCoord.getHighPrecLon() : leadCoord.getHighPrecLat();
                        final int leadAlong = isLongitude ? leadCoord.getHighPrecLat() : leadCoord.getHighPrecLon();
                        final int leadRel = Integer.signum(leadAway - dividingLine);
                        if (trailCoord != null) { // use first point as trailing (poly is closed)

                                Coord lineCoord = null;
                                int lineAlong = trailAlong; // initial assumption
                                if (trailRel == 0) // trailing point on line
                                        lineCoord = trailCoord;
                                else if (leadRel == 0) { // leading point on line
                                        lineCoord = leadCoord;
                                        lineAlong = leadAlong;
                                } else if (trailRel != leadRel) { // crosses line; make intersecting coord
                                        if (lineAlong != leadAlong)
                                                lineAlong += Math.round((double)(dividingLine - trailAway) * (leadAlong - trailAlong)
                                                                                  / (leadAway - trailAway));
                                        lineCoord = Coord.makeHighPrecCoord(isLongitude ? lineAlong : dividingLine, isLongitude ? dividingLine : lineAlong);
                                }
                                if (lineCoord != null && coordPool != null) {
                                        // Add new coords to pool. Also add existing ones if on the dividing line because there is slight
                                        // chance that the intersection will coincide with an existing point and ShapeMergeFilter expects
                                        // the opening/closing point to be the same object. If we see the original point first,
                                        // all is good, but if other way around, it will replace an original point with the created one.
                                        final long hashVal = Utils.coord2Long(lineCoord);
                                        final Coord replCoord = coordPool.get(hashVal);
                                        if (replCoord == null)
                                                coordPool.put(hashVal, lineCoord);
                                        else
                                                lineCoord = replCoord;
                                }

                                long extraArea; // add in later to get the area to leading point
                                if (leadRel * trailRel >= 0) // doesn't cross the line
                                        extraArea = (long)(trailAlong + leadAlong) * (trailAway - leadAway);
                                else { // calc as 2 points
                                        runningArea += (long)(trailAlong + lineAlong) * (trailAway - dividingLine);
                                        extraArea = (long)(lineAlong + leadAlong) * (dividingLine - leadAway);
                                }

                                if (wantLess) {
                                        if (leadRel < 0) { // this point required
                                                if (trailRel >= 0) // previous not on this side, add line point
                                                        openLine(newLess, lineCoord, lineAlong, runningArea);
                                                lessPoly.add(leadCoord);
                                        } else if (trailRel < 0) // if this not reqd and prev was, add line point and start new shape
                                                lessPoly = closeLine(newLess, lineCoord, lineAlong, runningArea + (leadRel == 0 ? extraArea : 0));
                                }

                                // identical to above except other way around
                                if (wantMore) {
                                        if (leadRel > 0) { // this point required
                                                if (trailRel <= 0) // previous not on this side, add line point
                                                        openLine(newMore, lineCoord, lineAlong, runningArea);
                                                morePoly.add(leadCoord);
                                        } else if (trailRel > 0) // if this not reqd and prev was, add line point and start new shape
                                                morePoly = closeLine(newMore, lineCoord, lineAlong, runningArea + (leadRel == 0 ? extraArea : 0));
                                }

                                runningArea += extraArea;
                        } // if not first Coord
                        trailCoord = leadCoord;
                        trailAway = leadAway;
                        trailAlong = leadAlong;
                        trailRel = leadRel;
                } // for leadCoord
                fullArea = runningArea;
        } // formLoops

        void logDiagInfo(List<Coord> coords, List<List<Coord>> lessList, List<List<Coord>> moreList) {
                int lowestPoint = newLess != null ? newLess.get(0).lowPoint : (newMore != null ? newMore.get(0).lowPoint : 0); // easier with small numbers
                log.info("#points:", coords.size(), "fullArea:", fullArea, "lowest:", lowestPoint, "gpxDir:", gpxDirectory);
                if (newLess != null)
                        for (MergeCloseHelper thisLine : newLess)
                                log.info("LessLoop", thisLine.lowPoint-lowestPoint, thisLine.highPoint-lowestPoint, thisLine.direction, thisLine.areaOrHole, thisLine.areaToLine, thisLine.points.size());
                if (newMore != null)
                        for (MergeCloseHelper thisLine : newMore)
                                log.info("MoreLoop", thisLine.lowPoint-lowestPoint, thisLine.highPoint-lowestPoint, thisLine.direction, thisLine.areaOrHole, thisLine.areaToLine, thisLine.points.size());
                if (log.isDebugEnabled()) {
                        uk.me.parabola.util.GpxCreator.createGpx(gpxDirectory + "S", coords); // original shape
                        int fInx = 0;
                        // NB: lessList/moreList could be non-existent, the same object or have already have contents
                        String filePrefix = lessList == moreList ? "B" : "L";
                        if (lessList != null)
                                for (List<Coord> fragment : lessList) {
                                        ++fInx;
                                        uk.me.parabola.util.GpxCreator.createGpx(gpxDirectory + filePrefix + fInx, fragment);
                                }
                        fInx = 0;
                        if (moreList != null && lessList != moreList)
                                for (List<Coord> fragment : moreList) {
                                        ++fInx;
                                        uk.me.parabola.util.GpxCreator.createGpx(gpxDirectory + "M" + fInx, fragment);
                                }
                }
        } // logDiagInfo

        // end of splitShape components

        /**
         * clip a shape with a rectangle
         *
         * Use above splitShape for each side; just keeping the 1/2 we want each time.
         *
         * @param coords the shape.
         * @param bounds the clipping area.
         * @param coordPool if not null, hashmap for created coordinates. Will all be on the edge.
         * @return list of shapes.
         */

        public static List<List<Coord>> clipToBounds(List<Coord> coords, Area bounds, Long2ObjectOpenHashMap<Coord> coordPool) {
                List<List<Coord>> newListA = new ArrayList<>();
                int dividingLine = bounds.getMinLat() << Coord.DELTA_SHIFT;
                splitShape(coords, dividingLine, false, null, newListA, coordPool);
                if (newListA.isEmpty())
                        return newListA;
                List<List<Coord>> newListB = new ArrayList<>();
                dividingLine = bounds.getMinLong() << Coord.DELTA_SHIFT;
                for (List<Coord> aShape : newListA)
                        splitShape(aShape, dividingLine, true, null, newListB, coordPool);
                if (newListB.isEmpty())
                        return newListB;
                newListA.clear();
                dividingLine = bounds.getMaxLat() << Coord.DELTA_SHIFT;
                for (List<Coord> aShape : newListB)
                        splitShape(aShape, dividingLine, false, newListA, null, coordPool);
                if (newListA.isEmpty())
                        return newListA;
                newListB.clear();
                dividingLine = bounds.getMaxLong() << Coord.DELTA_SHIFT;
                for (List<Coord> aShape : newListA)
                        splitShape(aShape, dividingLine, true, newListB, null, coordPool);
                return newListB;
        } // clipToBounds

} // ShapeSplitter