Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2017.
 *
 * 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.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Queue;

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

/**
 * Methods to cut an MP-relation so that holes are connected with the outer way(s).
 * Extracted from {@link MultiPolygonRelation}.
 *
 * @author WanMil
 * @author Gerd Petermann
 *
 */

public class MultiPolygonCutter {
        private static final Logger log = Logger.getLogger(MultiPolygonCutter.class);
        private final MultiPolygonRelation rel;
        private final Area tileArea;
        private final Long2ObjectOpenHashMap<Coord> commonCoordMap;

        /**
         * Create cutter for a given MP-relation and tile
         * @param multiPolygonRelation the MP-relation
         * @param tileArea the java area of the tile
         */

        public MultiPolygonCutter(MultiPolygonRelation multiPolygonRelation, Area tileArea, Long2ObjectOpenHashMap<Coord> commonCoordMap) {
                rel = multiPolygonRelation;
                this.tileArea = tileArea;
                this.commonCoordMap = commonCoordMap;

        }

        /**
         * Cut out all inner polygons from the outer polygon. This will divide the outer
         * polygon in several polygons.
         * @param multiPolygonRelation
         *
         * @param outerPolygon
         *            the outer polygon
         * @param innerPolygons
         *            a list of inner polygons
         * @return a list of polygons that make the outer polygon cut by the inner
         *         polygons
         */

        public List<Way> cutOutInnerPolygons(Way outerPolygon, List<Way> innerPolygons) {
                if (innerPolygons.isEmpty()) {
                        Way outerWay = new Way(outerPolygon.getId(), outerPolygon.getPoints());
                        if (log.isDebugEnabled()) {
                                log.debug("Way", outerPolygon.getId(), "splitted to way", outerWay.getId());
                        }
                        return Collections.singletonList(outerWay);
                }

                // use the java.awt.geom.Area class because it's a quick
                // implementation of what's needed

                // this list contains all non overlapping and singular areas
                // of the outerPolygon
                Queue<AreaCutData> areasToCut = new LinkedList<>();
                Collection<Area> finishedAreas = new ArrayList<>(innerPolygons.size());
               
                // create a list of Area objects from the outerPolygon (clipped to the bounding box)
                List<Area> outerAreas = createAreas(outerPolygon, true);
               
                // create the inner areas
                List<Area> innerAreas = new ArrayList<>(innerPolygons.size()+2);
                for (Way innerPolygon : innerPolygons) {
                        // don't need to clip to the bounding box because
                        // these polygons are just used to cut out holes
                        innerAreas.addAll(createAreas(innerPolygon, false));
                }

                // initialize the cut data queue
                if (innerAreas.isEmpty()) {
                        // this is a multipolygon without any inner areas
                        // nothing to cut
                        finishedAreas.addAll(outerAreas);
                } else if (outerAreas.size() == 1) {
                        // there is one outer area only
                        // it is assumed that all inner areas are inside this outer area
                        AreaCutData initialCutData = new AreaCutData();
                        initialCutData.outerArea = outerAreas.get(0);
                        initialCutData.innerAreas = innerAreas;
                        areasToCut.add(initialCutData);
                } else {
                        // initial data contains multiple outer areas
                        combineOuterAndInner(outerAreas, innerAreas, finishedAreas, areasToCut);
                }

                while (!areasToCut.isEmpty()) {
                        AreaCutData areaCutData = areasToCut.poll();
                        CutPoint cutPoint = calcNextCutPoint(areaCutData);
                       
                        if (cutPoint == null) {
                                finishedAreas.add(areaCutData.outerArea);
                                continue;
                        }
                       
                        assert cutPoint.getNumberOfAreas() > 0 : "Number of cut areas == 0 in mp " + rel.getId();
                       
                        // cut out the holes
                        if (cutPoint.getAreas().size() == 1)
                                areaCutData.outerArea.subtract(cutPoint.getAreas().get(0));
                        else {
                                // first combine the areas that should be subtracted
                                Path2D.Double path = new Path2D.Double(Path2D.WIND_NON_ZERO, innerAreas.size() * 20);
                                for (Area cutArea : cutPoint.getAreas()) {
                                        path.append(cutArea, false);
                                }
                                Area combinedCutAreas = new Area(path);
                                areaCutData.outerArea.subtract(combinedCutAreas);
                        }
                               
                        if (areaCutData.outerArea.isEmpty()) {
                                // this outer area space can be abandoned
                                continue;
                        }
                       
                        // the inner areas of the cut point have been processed
                        // they are no longer needed
                       
                        for (Area cutArea : cutPoint.getAreas()) {
                                ListIterator<Area> areaIter = areaCutData.innerAreas.listIterator();
                                while (areaIter.hasNext()) {
                                        Area a = areaIter.next();
                                        if (a == cutArea) {
                                                areaIter.remove();
                                                break;
                                        }
                                }
                        }

                        if (areaCutData.outerArea.isSingular()) {
                                // the area is singular
                                // => no further splits necessary
                                if (areaCutData.innerAreas.isEmpty()) {
                                        // this area is finished and needs no further cutting
                                        finishedAreas.add(areaCutData.outerArea);
                                } else {
                                        // read this area to further processing
                                        areasToCut.add(areaCutData);
                                }
                        } else {
                                // we need to cut the area into two halves to get singular areas
                                Rectangle2D r1 = cutPoint.getCutRectangleForArea(areaCutData.outerArea, true);
                                Rectangle2D r2 = cutPoint.getCutRectangleForArea(areaCutData.outerArea, false);

                                // Now find the intersection of these two boxes with the
                                // original polygon. This will make two new areas, and each
                                // area will be one (or more) polygons.
                                cutWithRectangle(r1, areaCutData, finishedAreas, areasToCut);
                                cutWithRectangle(r2, areaCutData, finishedAreas, areasToCut);
                        }
                }
               
                // convert the java.awt.geom.Area back to the mkgmap way
                List<Way> cuttedOuterPolygon = new ArrayList<>(finishedAreas.size());
                for (Area area : finishedAreas) {
                        Way w = singularAreaToWay(area, rel.getOriginalId());
                        if (w != null) {
                                w.markAsGeneratedFrom(rel);
                                w.copyTags(outerPolygon);
                                cuttedOuterPolygon.add(w);
                                if (log.isDebugEnabled()) {
                                        log.debug("Way", outerPolygon.getId(), "splitted to way", w.getId());
                                }
                        }
                }

                return cuttedOuterPolygon;
        }
       
        /**
         * Intersect area with cut rectangle.
         * Find the intersection of the rectangle with the original polygon and decide if the result is added to the
         * finished areas or if another cut is needed.
         *
         * @param cutRect the cut rectangle
         * @param areaCutData the cut data
         * @param finishedAreas list of finished areas
         * @param areasToCut queue with unfinished cut data
         */

        private static void cutWithRectangle(Rectangle2D cutRect, AreaCutData areaCutData, Collection<Area> finishedAreas,
                        Queue<AreaCutData> areasToCut) {
               
                Area outer = new Area(cutRect);
                outer.intersect(areaCutData.outerArea);
                final List<Area> dividedAreas = Java2DConverter.areaToSingularAreas(outer);
                if (areaCutData.innerAreas.isEmpty()) {
                        finishedAreas.addAll(dividedAreas);
                        return;
                }
                combineOuterAndInner(dividedAreas, areaCutData.innerAreas, finishedAreas, areasToCut);
        }

        /**
         * Combine each outer area which all those inner area that are likely to
         * intersect the outer. If no inner is found the outer area is finished, else
         * the new combination is added to the queue.
         *
         * @param outerAreas    list of outer areas
         * @param innerAreas    list of all inner areas
         * @param finishedAreas list of finished areas
         * @param areasToCut    queue with unfinished cut data
         */

        private static void combineOuterAndInner(List<Area> outerAreas, List<Area> innerAreas,
                        Collection<Area> finishedAreas, Queue<AreaCutData> areasToCut) {
                for (Area nextOuterArea : outerAreas) {
                        ArrayList<Area> nextInnerAreas = null;
                        // go through all remaining inner areas and check if they
                        // must be further processed with the nextOuterArea
                       
                        for (Area nonProcessedInner : innerAreas) {
                                if (nextOuterArea.intersects(nonProcessedInner.getBounds2D())) {
                                        if (nextInnerAreas == null) {
                                                nextInnerAreas = new ArrayList<>();
                                        }
                                        nextInnerAreas.add(nonProcessedInner);
                                }
                        }

                        if (nextInnerAreas == null) {
                                finishedAreas.add(nextOuterArea);
                        } else {
                                AreaCutData outCutData = new AreaCutData();
                                outCutData.outerArea = nextOuterArea;
                                outCutData.innerAreas = nextInnerAreas;
                                areasToCut.add(outCutData);
                        }
                }
        }

        private static CutPoint calcNextCutPoint(AreaCutData areaData) {
                if (areaData.innerAreas == null || areaData.innerAreas.isEmpty()) {
                        return null;
                }
               
                Rectangle2D outerBounds = areaData.outerArea.getBounds2D();
               
                if (areaData.innerAreas.size() == 1) {
                        // make it short if there is only one inner area
                        CutPoint cutPoint1 = new CutPoint(CoordinateAxis.LATITUDE, outerBounds);
                        cutPoint1.addArea(areaData.innerAreas.get(0));
                        CutPoint cutPoint2 = new CutPoint(CoordinateAxis.LONGITUDE, outerBounds);
                        cutPoint2.addArea(areaData.innerAreas.get(0));
                        if (cutPoint1.compareTo(cutPoint2) > 0) {
                                return cutPoint1;
                        } else {
                                return cutPoint2;
                        }
                }
               
                ArrayList<Area> innersSorted = new ArrayList<>(areaData.innerAreas);
               
                CutPoint bestCutPoint = null;
                for (CoordinateAxis axis : CoordinateAxis.values()) {
                        CutPoint currentCutPoint = new CutPoint(axis, outerBounds);

                        innersSorted.sort(axis == CoordinateAxis.LONGITUDE ? COMP_LONG_START: COMP_LAT_START);

                        for (Area inner : innersSorted) {
                                currentCutPoint.addArea(inner);

                                if (bestCutPoint == null || currentCutPoint.compareTo(bestCutPoint) > 0) {
                                        bestCutPoint = currentCutPoint.duplicate();
                                }
                        }
                }

                return bestCutPoint;
               
        }

        /**
         * Create the areas that are enclosed by the way. Usually the result should
         * only be one area but some ways contain intersecting lines. To handle these
         * erroneous cases properly the method might return a list of areas.
         *
         * @param w a closed way
         * @param clipBbox true if the areas should be clipped to the bounding box; false else
         * @return a list of enclosed ares
         */

        private List<Area> createAreas(Way w, boolean clipBbox) {
                Area area = Java2DConverter.createArea(w.getPoints());
                if (clipBbox && !tileArea.contains(area.getBounds2D())) {
                        // the area intersects the bounding box => clip it
                        area.intersect(tileArea);
                }
                List<Area> areaList = Java2DConverter.areaToSingularAreas(area);
                if (log.isDebugEnabled()) {
                        log.debug("Bbox clipped way",w.getId()+"=>",areaList.size(),"distinct area(s).");
                }
                return areaList;
        }

        /**
         * Convert an area to an mkgmap way. The caller must ensure that the area is singular.
         * Otherwise only the first part of the area is converted.
         *
         * @param area
         *            the area
         * @param wayId
         *            the wayid for the new way
         * @return a new mkgmap way
         */

        private Way singularAreaToWay(Area area, long wayId) {
                // make sure that equal coords are changed to identical coord instances
                // this allows merging in the ShapeMerger
                List<Coord> points = Java2DConverter.singularAreaToPoints(area, commonCoordMap);
                if (points == null || points.isEmpty()) {
                        if (log.isDebugEnabled()) {
                                log.debug("Empty area", wayId + ".", rel.toBrowseURL());
                        }
                        return null;
                }

                return new Way(wayId, points);
        }
        private static class AreaCutData {
                Area outerArea;
                List<Area> innerAreas;
        }

        private static final int CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD = 1<<(11 + Coord.DELTA_SHIFT);
        private static final int CUT_POINT_CLASSIFICATION_BAD_THRESHOLD = 1<< (8 + Coord.DELTA_SHIFT);
        private static class CutPoint implements Comparable<CutPoint>{
                private int startPoinHp = Integer.MAX_VALUE; // high precision map units
                private int stopPointHp = Integer.MIN_VALUE;  // high precision map units
                private Integer cutPointHp = null; // high precision map units
                private final LinkedList<Area> areas;
                private final Comparator<Area> comparator;
                private final CoordinateAxis axis;
                private Rectangle2D bounds;
                private final Rectangle2D outerBounds;
                private Double minAspectRatio;

                public CutPoint(CoordinateAxis axis, Rectangle2D outerBounds) {
                        this.axis = axis;
                        this.outerBounds = outerBounds;
                        this.areas = new LinkedList<>();
                        this.comparator = (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_STOP : COMP_LAT_STOP);
                }
               
                public CutPoint duplicate() {
                        CutPoint newCutPoint = new CutPoint(this.axis, this.outerBounds);
                        newCutPoint.areas.addAll(areas);
                        newCutPoint.startPoinHp = startPoinHp;
                        newCutPoint.stopPointHp = stopPointHp;
                        return newCutPoint;
                }

                private boolean isGoodCutPoint() {
                        // It is better if the cutting line is on a multiple of 2048.
                        // Otherwise MapSource and QLandkarteGT paints gaps between the cuts
                        return getCutPointHighPrec() % CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD == 0;
                }
               
                private boolean isBadCutPoint() {
                        int d1 = getCutPointHighPrec() - startPoinHp;
                        int d2 = stopPointHp - getCutPointHighPrec();
                        return Math.min(d1, d2) < CUT_POINT_CLASSIFICATION_BAD_THRESHOLD;
                }
               
                /**
                 * Calculates the point where the cut should be applied.
                 * @return the point of cut
                 */

                private int getCutPointHighPrec() {
                        if (cutPointHp != null) {
                                // already calculated => just return it
                                return cutPointHp;
                        }
                       
                        if (startPoinHp == stopPointHp) {
                                // there is no choice => return the one possible point
                                cutPointHp = startPoinHp;
                                return cutPointHp;
                        }
                       
                        // try to cut with a good aspect ratio so try the middle of the polygon to be cut
                        int midOuterHp = axis.getStartHighPrec(outerBounds)+(axis.getStopHighPrec(outerBounds) - axis.getStartHighPrec(outerBounds)) / 2;
                        cutPointHp = midOuterHp;

                        if (midOuterHp < startPoinHp) {
                                // not possible => the start point is greater than the middle so correct to the startPoint
                                cutPointHp = startPoinHp;
                               
                                if (((cutPointHp & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1)) + CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD) <= stopPointHp) {
                                        cutPointHp = ((cutPointHp & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1)) + CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD);
                                }
                               
                        } else if (midOuterHp > stopPointHp) {
                                // not possible => the stop point is smaller than the middle so correct to the stopPoint
                                cutPointHp = stopPointHp;

                                if ((cutPointHp & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1))  >= startPoinHp) {
                                        cutPointHp = (cutPointHp & ~(CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD-1));
                                }
                        }
                       
                       
                        // try to find a cut point that is a multiple of 2048 to
                        // avoid that gaps are painted by MapSource and QLandkarteGT
                        // between the cutting lines
                        int cutMod = cutPointHp % CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD;
                        if (cutMod == 0) {
                                return cutPointHp;
                        }
                       
                        int cut1 = (cutMod > 0 ? cutPointHp-cutMod : cutPointHp  - CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD- cutMod);
                        if (cut1 >= startPoinHp && cut1 <= stopPointHp) {
                                cutPointHp = cut1;
                                return cutPointHp;
                        }
                       
                        int cut2 = (cutMod > 0 ? cutPointHp + CUT_POINT_CLASSIFICATION_GOOD_THRESHOLD -cutMod : cutPointHp - cutMod);
                        if (cut2 >= startPoinHp && cut2 <= stopPointHp) {
                                cutPointHp = cut2;
                                return cutPointHp;
                        }
                       
                        return cutPointHp;
                }

                public Rectangle2D getCutRectangleForArea(Area toCut, boolean firstRect) {
                        return getCutRectangleForArea(toCut.getBounds2D(), firstRect);
                }
               
                public Rectangle2D getCutRectangleForArea(Rectangle2D areaRect, boolean firstRect) {
                        double cp = (double)  getCutPointHighPrec() / (1<<Coord.DELTA_SHIFT);
                        if (axis == CoordinateAxis.LONGITUDE) {
                                double newWidth = cp-areaRect.getX();
                                if (firstRect) {
                                        return new Rectangle2D.Double(areaRect.getX(), areaRect.getY(), newWidth, areaRect.getHeight());
                                } else {
                                        return new Rectangle2D.Double(areaRect.getX()+newWidth, areaRect.getY(), areaRect.getWidth()-newWidth, areaRect.getHeight());
                                }
                        } else {
                                double newHeight = cp-areaRect.getY();
                                if (firstRect) {
                                        return new Rectangle2D.Double(areaRect.getX(), areaRect.getY(), areaRect.getWidth(), newHeight);
                                } else {
                                        return new Rectangle2D.Double(areaRect.getX(), areaRect.getY()+newHeight, areaRect.getWidth(), areaRect.getHeight()-newHeight);
                                }
                        }
                }
               
                public List<Area> getAreas() {
                        return areas;
                }

                public void addArea(Area area) {
                        // remove all areas that do not overlap with the new area
                        while (!areas.isEmpty() && axis.getStopHighPrec(areas.getFirst()) < axis.getStartHighPrec(area)) {
                                // remove the first area
                                areas.removeFirst();
                        }

                        areas.add(area);
                        areas.sort(comparator);
                        startPoinHp = axis.getStartHighPrec(Collections.max(areas,
                                (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_START
                                                : COMP_LAT_START)));
                        stopPointHp = axis.getStopHighPrec(areas.getFirst());
                       
                        // reset the cached value => need to be recalculated the next time they are needed
                        bounds = null;
                        cutPointHp = null;
                        minAspectRatio = null;
                }

                public int getNumberOfAreas() {
                        return this.areas.size();
                }

                /**
                 * Retrieves the minimum aspect ratio of the outer bounds after cutting.
                 *
                 * @return minimum aspect ratio of outer bound after cutting
                 */

                public double getMinAspectRatio() {
                        if (minAspectRatio == null) {
                                // first get the left/upper cut
                                Rectangle2D r1 = getCutRectangleForArea(outerBounds, true);
                                double s11 = CoordinateAxis.LATITUDE.getSizeOfSide(r1);
                                double s12 = CoordinateAxis.LONGITUDE.getSizeOfSide(r1);
                                double ar1 = Math.min(s11, s12) / Math.max(s11, s12);

                                // second get the right/lower cut
                                Rectangle2D r2 = getCutRectangleForArea(outerBounds, false);
                                double s21 = CoordinateAxis.LATITUDE.getSizeOfSide(r2);
                                double s22 = CoordinateAxis.LONGITUDE.getSizeOfSide(r2);
                                double ar2 = Math.min(s21, s22) / Math.max(s21, s22);

                                // get the minimum
                                minAspectRatio = Math.min(ar1, ar2);
                        }
                        return minAspectRatio;
                }
               
                public int compareTo(CutPoint o) {
                        if (this == o) {
                                return 0;
                        }
                        // handle the special case that a cut has no area
                        if (getNumberOfAreas() == 0) {
                                return o.getNumberOfAreas() == 0 ? 0 : -1;
                        } else if (o.getNumberOfAreas() == 0) {
                                return 1;
                        }

                        int d = Boolean.compare(o.isBadCutPoint(), isBadCutPoint()); // exchanged order!
                        if (d != 0)
                                return d;
                        double dAR = getMinAspectRatio() - o.getMinAspectRatio();
                        if (dAR != 0) {
                                return (dAR > 0 ? 1 : -1);
                        }
                        d = Boolean.compare(isGoodCutPoint(), o.isGoodCutPoint());
                        if (d != 0)
                                return d;
                        // prefer the larger area that is split
                        d = Double.compare(axis.getSizeOfSide(getBounds2D()), o.axis.getSizeOfSide(o.getBounds2D()));
                        if (d != 0)
                                return d;

                        return getNumberOfAreas() - o.getNumberOfAreas();

                }

                private Rectangle2D getBounds2D() {
                        if (bounds == null) {
                                // lazy init
                                bounds = new Rectangle2D.Double();
                                for (Area a : areas)
                                        bounds.add(a.getBounds2D());
                        }
                        return bounds;
                }

                public String toString() {
                        return axis +" "+getNumberOfAreas()+" "+startPoinHp+" "+stopPointHp+" "+getCutPointHighPrec();
                }
        }

        private enum CoordinateAxis {
                LATITUDE(false), LONGITUDE(true);

                private CoordinateAxis(boolean useX) {
                        this.useX = useX;
                }

                private final boolean useX;

                public int getStartHighPrec(Area area) {
                        return getStartHighPrec(area.getBounds2D());
                }

                public int getStartHighPrec(Rectangle2D rect) {
                        double val = (useX ? rect.getX() : rect.getY());
                        return (int)Math.round(val * (1<<Coord.DELTA_SHIFT));
                }

                public int getStopHighPrec(Area area) {
                        return getStopHighPrec(area.getBounds2D());
                }

                public int getStopHighPrec(Rectangle2D rect) {
                        double val = (useX ? rect.getMaxX() : rect.getMaxY());
                        return (int)Math.round(val * (1<<Coord.DELTA_SHIFT));
                }
               
                public double getSizeOfSide(Rectangle2D rect) {
                        if (useX) {
                                int latHp = (int)Math.round(rect.getY() * (1<<Coord.DELTA_SHIFT));
                                Coord c1 = Coord.makeHighPrecCoord(latHp, getStartHighPrec(rect));
                                Coord c2 = Coord.makeHighPrecCoord(latHp, getStopHighPrec(rect));
                                return c1.distance(c2);
                        } else {
                                int lonHp = (int)Math.round(rect.getX() * (1<<Coord.DELTA_SHIFT));
                                Coord c1 = Coord.makeHighPrecCoord(getStartHighPrec(rect), lonHp);
                                Coord c2 = Coord.makeHighPrecCoord(getStopHighPrec(rect), lonHp);
                                return c1.distance(c2);
                        }
                }
        }
       
        private static final AreaComparator COMP_LONG_START = new AreaComparator(
                        true, CoordinateAxis.LONGITUDE);
        private static final AreaComparator COMP_LONG_STOP = new AreaComparator(
                        false, CoordinateAxis.LONGITUDE);
        private static final AreaComparator COMP_LAT_START = new AreaComparator(
                        true, CoordinateAxis.LATITUDE);
        private static final AreaComparator COMP_LAT_STOP = new AreaComparator(
                        false, CoordinateAxis.LATITUDE);

        private static class AreaComparator implements Comparator<Area> {

                private final CoordinateAxis axis;
                private final boolean startPoint;

                public AreaComparator(boolean startPoint, CoordinateAxis axis) {
                        this.startPoint = startPoint;
                        this.axis = axis;
                }

                public int compare(Area o1, Area o2) {
                        if (o1 == o2) {
                                return 0;
                        }

                        if (startPoint) {
                                int cmp = axis.getStartHighPrec(o1) - axis.getStartHighPrec(o2);
                                if (cmp == 0) {
                                        return axis.getStopHighPrec(o1) - axis.getStopHighPrec(o2);
                                } else {
                                        return cmp;
                                }
                        } else {
                                int cmp = axis.getStopHighPrec(o1) - axis.getStopHighPrec(o2);
                                if (cmp == 0) {
                                        return axis.getStartHighPrec(o1) - axis.getStartHighPrec(o2);
                                } else {
                                        return cmp;
                                }
                        }
                }

        }
}