Subversion Repositories splitter

Rev

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

/*
 * Copyright (c) 2009, Chris Miller
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 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.splitter.solver;

import it.unimi.dsi.fastutil.ints.IntArrayList;
import uk.me.parabola.splitter.Area;
import uk.me.parabola.splitter.RoundingUtils;
import uk.me.parabola.splitter.SplitFailedException;
import uk.me.parabola.splitter.Utils;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;

/**
 * Splits a density map into multiple areas, none of which
 * exceed the desired threshold.
 *
* @author Chris Miller, Gerd Petermann
 */

public class SplittableDensityArea {
        private static final int MAX_LAT_DEGREES = 85;
        private static final int MAX_LON_DEGREES = 90;
        public static final int MAX_SINGLE_POLYGON_VERTICES = 40;
        private static final int MAX_LOOPS = 100;       // number of loops to find better solution for one rectangular area
        private static final int AXIS_HOR = 0;
        private static final int AXIS_VERT = 1;
        public static final double NICE_MAX_ASPECT_RATIO = 4;
        private static final double VERY_NICE_FILL_RATIO = 0.93;
        private static final long LARGE_MAX_NODES = 10_000_000;
        private static final int GOOD_SOL_INIT_SIZE = 1_000_000;
       
        private double maxAspectRatio;
        private long minNodes;
        private final int startSearchLimit;
        private int searchLimit;
        private double maxOutsidePolygonRatio = 0.5; // TODO: maybe reduce it when a good solution was found

        private final DensityMap allDensities;
        private EnhancedDensityMap extraDensityInfo;
       
        private boolean beQuiet = false;
        private long maxNodes;
        private final int shift;
       
        private HashSet<Tile> knownBad;
        private LinkedHashMap<Tile, Integer> incomplete;
        private long countBad;
       
        /** if true enables an alternative algorithm */
        private boolean searchAll = false;
       
        final int maxTileHeight;
        final int maxTileWidth;
       
        private HashMap<Tile,Solution> goodSolutions;
        private double goodRatio;
        private boolean trimShape;
        private boolean trimTiles;
        private boolean allowEmptyPart = false;
        private int currMapId;
        private boolean hasEmptyPart;
        private boolean ignoreSize;
       
        public SplittableDensityArea(DensityMap densities, int startSearchLimit) {
                this.shift = densities.getShift();
                this.searchLimit = this.startSearchLimit = startSearchLimit;
                maxTileHeight = Utils.toMapUnit(MAX_LAT_DEGREES) / (1 << shift);
                maxTileWidth = Utils.toMapUnit(MAX_LON_DEGREES) / (1 << shift);
                allDensities = densities;
        }
        public DensityMap getAllDensities() {
                return allDensities;
        }
        public void setMapId(int mapId) {
                currMapId = mapId;
        }

        public void setMaxNodes(long maxNodes) {
                this.maxNodes = maxNodes;
        }


        public void setTrim(boolean trim) {
                this.trimShape = trim;
        }

        public boolean hasData(){
                return allDensities != null && allDensities.getNodeCount() > 0;
        }

        /**
         * @return the area that this splittable area represents
         */
   
        public Area getBounds() {
                return allDensities.getBounds();
        }

        /**
         * @param maxNodes the maximum number of nodes per area
         * @return a list of areas, each containing no more than {@code maxNodes} nodes.
         * Each area returned must be aligned to the appropriate overview map resolution.
         */
   
        private List<Area> split() {
                if (allDensities == null || allDensities.getNodeCount() == 0)
                        return Collections.emptyList();
                prepare(null);
                Tile startTile = new Tile(extraDensityInfo);
                List<Tile> startTiles = new ArrayList<>();
                if (trimShape || allDensities.getBounds().getWidth() >= 0x1000000){
                        // if trim is wanted or tile spans over planet
                        // we try first to find large empty areas (sea)
                        startTiles.addAll(checkForEmptyClusters(0, startTile, true));
                }
                else
                        startTiles.add(startTile);
               
                Solution fullSolution = new Solution(maxNodes);
                int countNoSol;
                while (true){
                        countNoSol = 0;
                        for (Tile tile: startTiles){
                                hasEmptyPart = false;
                                if (!beQuiet)
                                        System.out.println("Solving partition " + tile.toString());
                                Solution solution = solveRectangularArea(tile);
                                if (solution != null && solution.isEmpty() == false)
                                        fullSolution.merge(solution);
                                else {
                                        countNoSol++;
                                        if (!beQuiet)
                                                System.out.println("Warning: No solution found for partition " + tile.toString());
                                }
                        }
                        if (countNoSol == 0)
                                break;
                        if (allowEmptyPart || hasEmptyPart == false)
                                break;
                        allowEmptyPart = true;
                        fullSolution = new Solution(maxNodes);
                }
                if(countNoSol > 0)
                        throw new SplitFailedException("Failed to find a correct split");
                System.out.println("Final solution: " +  fullSolution.toString());
                if (fullSolution.isNice())
                        System.out.println("This seems to be nice.");
                return getAreas(fullSolution, null);
        }

        /**
         * Split with a given polygon and max nodes threshold. If the polygon
         * is not singular, it is divided into singular areas.
         * @param maxNodes
         * @param polygonArea
         * @return
         */

        private List<Area> split(java.awt.geom.Area polygonArea) {
                if (polygonArea == null)
                        return split();
                if (polygonArea.isSingular()){
                        java.awt.geom.Area rasteredArea = allDensities.rasterPolygon(polygonArea);
                        if (rasteredArea.isEmpty()){
                                System.err.println("Bounding polygon doesn't intersect with the bounding box of the input file(s)");
                                return Collections.emptyList();
                        }
                       
                        prepare(polygonArea);
                        Tile tile = new Tile(extraDensityInfo, rasteredArea.getBounds());
                        Solution solution = findSolutionWithSinglePolygon(0, tile, rasteredArea);
                        return getAreas(solution, polygonArea);
                }
                if (polygonArea.intersects(Utils.area2Rectangle(allDensities.getBounds(),0)))
                        return splitPolygon(polygonArea);
                System.err.println("Bounding polygon doesn't intersect with the bounding box of the input file(s)");
                return Collections.emptyList();
        }

        /**
         * Split a list of named polygons. Overlapping areas of the polygons are
         * extracted and each one is split for itself. A polygon may not be singular.
         * @param maxNodes
         * @param namedPolygons
         * @return
         */

        public List<Area> split(List<PolygonDesc> namedPolygons) {
                if (namedPolygons.isEmpty())
                        return split();
                List<Area> result = new ArrayList<>();
                class ShareInfo {
                        java.awt.geom.Area area;
                        final IntArrayList sharedBy = new IntArrayList();
                }
                List<ShareInfo> sharedParts = new ArrayList<>();
                for (int i = 0; i < namedPolygons.size(); i++){
                        boolean wasDistinct = true;
                        PolygonDesc namedPart = namedPolygons.get(i);
                        java.awt.geom.Area distinctPart = new java.awt.geom.Area (namedPart.getArea());
                        for(int j = 0; j < namedPolygons.size(); j++){
                                if (j == i)
                                        continue;
                                java.awt.geom.Area test = new java.awt.geom.Area(namedPart.getArea());
                                test.intersect(namedPolygons.get(j).getArea());
                                if (test.isEmpty() == false){
                                        wasDistinct = false;
                                        distinctPart.subtract(namedPolygons.get(j).getArea());
                                        if (j > i){
                                                ShareInfo si = new ShareInfo();
                                                si.area = test;
                                                si.sharedBy.add(i);
                                                si.sharedBy.add(j);
                                                sharedParts.add(si);
                                        }
                                }
                        }
                        if (distinctPart.isEmpty() == false && distinctPart.intersects(Utils.area2Rectangle(allDensities.getBounds(),0))){
//                              KmlWriter.writeKml("e:/ld_sp/distinct_"+namedPart.name, "distinct", distinctPart);
                                if (wasDistinct == false)
                                        System.out.println("splitting distinct part of " + namedPart.getName());
                                else
                                        System.out.println("splitting " + namedPart.getName());
                                result.addAll(split(distinctPart));
                        }
                }
               
                for (int i = 0; i < sharedParts.size(); i++){
                        ShareInfo si = sharedParts.get(i);
                        int last = namedPolygons.size(); // list is extended in the loop
                        for (int j = 0; j < last; j++){
                                if (si.sharedBy.contains(j))
                                        continue;
                                java.awt.geom.Area test = new java.awt.geom.Area(si.area);
                                test.intersect(namedPolygons.get(j).getArea());
                                if (test.isEmpty() == false){
                                        si.area.subtract(test);
                                        if (j > si.sharedBy.getInt(si.sharedBy.size()-1)){
                                                ShareInfo si2 = new ShareInfo();
                                                si2.area = test;
                                                si2.sharedBy.addAll(si.sharedBy);
                                                si2.sharedBy.add(j);
                                                sharedParts.add(si2);
                                        }
                                }
                                if (si.area.isEmpty())
                                        break;
                        }
                        if (si.area.isEmpty() == false && si.area.intersects(Utils.area2Rectangle(allDensities.getBounds(),0))){
                                String desc = "";
                                for (int pos : si.sharedBy)
                                        desc += namedPolygons.get(pos).getName() + " and ";
                                desc = desc.substring(0,desc.lastIndexOf(" and"));
                                System.out.println("splitting area shared by exactly " + si.sharedBy.size() + " polygons: " + desc);
//                              KmlWriter.writeKml("e:/ld_sp/shared_"+desc.replace(" " , "_"), desc, si.area);
                                result.addAll(split(si.area));
                        }
                }
                return result;
        }

        /**
         * Split a list of named polygons into a given number of tiles.
         * This is probably only useful with an empty list of polygons
         * or a list containing one polygon.
         * @param wantedTiles
         * @return
         */

        public List<Area> split(int wantedTiles) {
                long currMaxNodes = this.allDensities.getNodeCount() / wantedTiles;
                class Pair {
                        long maxNodes;
                        int numTiles;
                       
                        Pair(long maxNodes, int numTiles){
                                this.maxNodes = maxNodes;
                                this.numTiles = numTiles;  
                        }
                }
                Pair bestBelow = null;
                Pair bestAbove = null;
                beQuiet = true;
                ignoreSize = true;
                while (true) {
                        setMaxNodes(currMaxNodes);
                        System.out.println("Trying a max-nodes value of " + currMaxNodes + " to split " + allDensities.getNodeCount() + " nodes into " + wantedTiles + " areas");
                        List<Area> res = split();
                        if (res.isEmpty() || res.size() == wantedTiles){
                                beQuiet = false;
                                res = split();
                                return res;
                        }
                        goodSolutions = new HashMap<>(GOOD_SOL_INIT_SIZE);
                        Pair pair = new Pair(currMaxNodes, res.size());
                        if (res.size() > wantedTiles){
                                if (bestAbove == null)
                                        bestAbove = pair;
                                else if (bestAbove.numTiles > pair.numTiles)
                                        bestAbove = pair;
                                else  if (bestAbove.numTiles == pair.numTiles && pair.maxNodes < bestAbove.maxNodes)
                                        bestAbove = pair;
                        } else {
                                if (bestBelow == null)
                                        bestBelow = pair;
                                else if (bestBelow.numTiles < pair.numTiles)
                                        bestBelow = pair;
                                else  if (bestBelow.numTiles == pair.numTiles && pair.maxNodes > bestBelow.maxNodes)
                                        bestBelow = pair;
                        }
                        long testMaxNodes;
                        if (bestBelow == null || bestAbove == null)
                                testMaxNodes = Math.min(Math.round((double) currMaxNodes * res.size() / wantedTiles), this.allDensities.getNodeCount()-1);
                        else
                                testMaxNodes = (bestBelow.maxNodes + bestAbove.maxNodes) / 2;
                        if (testMaxNodes == currMaxNodes){
                                System.err.println("Cannot find a good split with exactly " + wantedTiles + " areas");
                                return res;
                        }
                        currMaxNodes = testMaxNodes;
                }
        }

        /**
         * Filter the density data, calculate once complex trigonometric results
         * @param polygonArea
         */

        private void prepare(java.awt.geom.Area polygonArea){
                extraDensityInfo = new EnhancedDensityMap(allDensities, polygonArea);
                if (!beQuiet){
                        System.out.println("Highest node count in a single grid element is "
                                        + Utils.format(extraDensityInfo.getMaxNodesInDensityMapGridElement()));
                        if (polygonArea != null)
                        System.out.println("Highest node count in a single grid element within the bounding polygon is "
                                        + Utils.format(extraDensityInfo.getMaxNodesInDensityMapGridElementInPoly()));
                }
                if (polygonArea != null)
                        trimTiles = true;

        }

        /**
         * Check if the solution should be stored in the map of partial good solutions
         * @param tile the tile for which the solution was found
         * @param sol the solution for the tile
         */

        private void checkIfGood(Tile tile, Solution sol){
                if (sol.isNice() == false || sol.getTiles().size() < 2)
                        return;
                if (sol.getWorstMinNodes() > (goodRatio * maxNodes)){
                        Solution good = sol.copy();
                        // add new or replace worse solution
                        goodSolutions.compute(tile, (k,v) -> v == null || v.getWorstMinNodes() < good.getWorstMinNodes() ? good : v);
                }
               
        }

        /**
         * Remove entries from the map of partial good solutions which
         * cannot help to improve the best solution.
         * @param best the best known solution
         */

        private void filterGoodSolutions(Solution best){
                if (best == null || best.isEmpty())
                        return;
                goodSolutions.entrySet().removeIf(entry -> entry.getValue().getWorstMinNodes() <= best.getWorstMinNodes());
                goodRatio = Math.max(0.5, (double) best.getWorstMinNodes() / maxNodes);
        }

        /**
         * Search a solution for the given tile in the map of partial good solutions
         * @param tile the tile to split
         * @return a copy of the best known solution or null
         */

        private Solution searchGoodSolutions(Tile tile){
                Solution sol = goodSolutions.get(tile);
                if (sol != null){
                        if (sol.getWorstMinNodes() < minNodes)
                                return null;
                        sol = sol.copy();
                }
                return sol;
        }
       
       
        /**
         * Try to find empty areas. This will fail if the empty area is enclosed by a
         * non-empty area.
         * @param depth recursion depth
         * @param tile the tile that might contain an empty area
         * @param splitHoriz true: search horizontal, else vertical
         * @return a list containing one or more tiles, cut from the original tile, or
         * just the original tile
         */

        private ArrayList<Tile> checkForEmptyClusters(int depth, final Tile tile, boolean splitHoriz) {
                java.awt.geom.Area area = new java.awt.geom.Area(tile);
                int firstEmpty = -1;
                int countEmpty = 0;
                long countLastPart = 0;
                long countRemaining = tile.getCount();
                long maxEmpty = Utils.toMapUnit(30) / (1 << shift);
                if (splitHoriz){
                        for (int i = 0; i < tile.width; i++){
                                long count = tile.getColSum(i);
                                if (count == 0){
                                        if (firstEmpty < 0)
                                                firstEmpty = i;
                                        countEmpty++;
                                } else {
                                        if (countEmpty > maxEmpty || (countEmpty > 10 && countLastPart > maxNodes/3 && countRemaining > maxNodes/3)){
                                                java.awt.geom.Area empty = new java.awt.geom.Area(new Rectangle(firstEmpty,tile.y,countEmpty,tile.height));
                                                area.subtract(empty);
                                                countLastPart = 0;
                                        }
                                        countRemaining -= count;
                                        firstEmpty = -1;
                                        countEmpty = 0;
                                        countLastPart += count;
                                }
                        }
                } else {
                        for (int i = 0; i < tile.height; i++){
                                long count = tile.getRowSum(i);
                                if (count == 0){
                                        if (firstEmpty < 0)
                                                firstEmpty = i;
                                        countEmpty++;
                                } else {
                                        if (countEmpty > maxEmpty || (countEmpty > 10 && countLastPart > maxNodes/3 && countRemaining > maxNodes/3)){
                                                java.awt.geom.Area empty = new java.awt.geom.Area(new Rectangle(tile.x,firstEmpty,tile.width,countEmpty));
                                                area.subtract(empty);
                                                countLastPart = 0;
                                        }
                                        countRemaining -= count;
                                        firstEmpty = -1;
                                        countEmpty = 0;
                                        countLastPart += count;
                                }
                        }
                }
                ArrayList<Tile> clusters = new ArrayList<>();
                if (depth == 0 && area.isSingular()){
                        // try also the other split axis
                        clusters.addAll(checkForEmptyClusters(depth + 1, tile.trim(), !splitHoriz ));
                } else {
                        if (area.isSingular()){
                                clusters.add(tile.trim());
                        } else {
                                List<List<Point>> shapes = Utils.areaToShapes(area);
                                for (List<Point> shape: shapes){
                                        java.awt.geom.Area part = Utils.shapeToArea(shape);
                                        Tile t = new Tile(extraDensityInfo, part.getBounds());
                                        if (t.getCount() > 0)
                                                clusters.addAll(checkForEmptyClusters(depth + 1, t.trim(), !splitHoriz ));
                                }
                        }
                }
                return clusters;
        }

        /**
         * Split, handling a polygon that may contain multiple distinct areas.
         * @param polygonArea
         * @return a list of areas that cover the polygon
         */

        private List<Area> splitPolygon(final java.awt.geom.Area polygonArea) {
                List<Area> result = new ArrayList<>();
                List<List<Point>> shapes = Utils.areaToShapes(polygonArea);
                for (int i = 0; i < shapes.size(); i++){
                        List<Point> shape = shapes.get(i);
                        if (Utils.clockwise(shape) == false)
                                continue;
                        java.awt.geom.Area shapeArea = Utils.shapeToArea(shape);
                        Rectangle rShape = shapeArea.getBounds();
                        if (shape.size() > MAX_SINGLE_POLYGON_VERTICES){
                                shapeArea = new java.awt.geom.Area(rShape);
                                System.out.println("Warning: shape is too complex, using rectangle " + rShape+ " instead");
                        }
                        Area shapeBounds = new Area(rShape.y, rShape.x,(int)rShape.getMaxY(), (int)rShape.getMaxX());
                        int resolution = 24-allDensities.getShift();
                        shapeBounds  = RoundingUtils.round(shapeBounds, resolution);
                        SplittableDensityArea splittableArea = new SplittableDensityArea(allDensities.subset(shapeBounds), startSearchLimit);
                        splittableArea.setMaxNodes(maxNodes);
                        if (splittableArea.hasData() == false){
                                System.out.println("Warning: a part of the bounding polygon would be empty and is ignored:" + shapeBounds);
                                //result.add(shapeBounds);
                                continue;
                        }
                        List<Area> partResult = splittableArea.split(shapeArea);
                        if (partResult != null)
                                result.addAll(partResult);
                }
                return result;
        }
       

        /**
         * Split the given tile using the given (singular) polygon area. The routine splits the polygon into parts
         * and calls itself recursively for each part that is not rectangular.
         * @param depth recursion depth
         * @param tile the tile to split
         * @param rasteredPolygonArea an area describing a rectilinear shape
         * @return a solution (maybe empty)
         */

        private Solution findSolutionWithSinglePolygon(int depth, final Tile tile, java.awt.geom.Area rasteredPolygonArea) {
                assert rasteredPolygonArea.isSingular();
                if (rasteredPolygonArea.isRectangular()){
                        Tile part = new Tile(extraDensityInfo, rasteredPolygonArea.getBounds());
                        return solveRectangularArea(part);
                }
                List<List<Point>> shapes = Utils.areaToShapes(rasteredPolygonArea);
                List<Point> shape = shapes.get(0);
               
                if (shape.size() > MAX_SINGLE_POLYGON_VERTICES){
                        Tile part = new Tile(extraDensityInfo, rasteredPolygonArea.getBounds());
                        System.out.println("Warning: shape is too complex, using rectangle " + part + " instead");
                        return solveRectangularArea(part);
                }
               
                Rectangle pBounds = rasteredPolygonArea.getBounds();
                int lastPoint = shape.size() - 1;
                if (shape.get(0).equals(shape.get(lastPoint)))
                        --lastPoint;
                for (int i = 0; i <= lastPoint; i++){
                        Point point = shape.get(i);
                        if (i > 0 && point.equals(shape.get(0)))
                                continue;
                        int cutX = point.x;
                        int cutY = point.y;
                        Solution part0Sol = null,part1Sol = null;
                        for (int axis = 0; axis < 2; axis++){
                                Rectangle r1,r2;
                                if (axis == AXIS_HOR){
                                        r1 = new Rectangle(pBounds.x,pBounds.y,cutX-pBounds.x,pBounds.height);
                                        r2 = new Rectangle(cutX,pBounds.y,(int)(pBounds.getMaxX()-cutX),pBounds.height);
                                } else {
                                        r1 = new Rectangle(pBounds.x,pBounds.y,pBounds.width,cutY-pBounds.y);
                                        r2 = new Rectangle(pBounds.x,cutY,pBounds.width,(int)(pBounds.getMaxY()-cutY));
                                }

                                if (r1.width * r1.height> r2.width * r2.height){
                                        Rectangle help = r1;
                                        r1 = r2;
                                        r2 = help;
                                }
                                if (r1.isEmpty() == false && r2.isEmpty() == false){
                                        java.awt.geom.Area area = new java.awt.geom.Area(r1);
                                        area.intersect(rasteredPolygonArea);
                                       
                                        part0Sol = findSolutionWithSinglePolygon(depth+1, tile, area);
                                        if (part0Sol != null && part0Sol.isEmpty() == false){
                                                area = new java.awt.geom.Area(r2);
                                                area.intersect(rasteredPolygonArea);
                                                part1Sol = findSolutionWithSinglePolygon(depth+1, tile, area);
                                                if (part1Sol != null && part1Sol.isEmpty() == false)
                                                        break;
                                        }
                                }
                        }
                        if (part1Sol != null){
                                part0Sol.merge(part1Sol);
                                return part0Sol;
                        }
                }
                return new Solution(maxNodes);
        }
       
        /**
         * Try to split the tile into nice parts recursively.
         * @param depth the recursion depth
         * @param tile the tile to be split
         * @return a solution instance or null
         */

        private Solution findSolution(int depth, final Tile tile, Tile parent, TileMetaInfo smiParent){
                boolean addAndReturn = false;
                if (tile.getCount() == 0){
                        if (!allowEmptyPart){
                                hasEmptyPart  = true;
                                return null;
                        }
                        if  (tile.width * tile.height <= 4)
                                return null;
                        return new Solution(maxNodes); // allow empty part of the world
                } else if (tile.getCount() > maxNodes && tile.width == 1 && tile.height == 1) {
                        addAndReturn = true;  // can't split further
                } else if (tile.getCount() < minNodes && depth == 0) {
                        addAndReturn = true; // nothing to do
                } else if (tile.getCount() < minNodes) {
                        return null;
                } else if (tile.getCount() <= maxNodes) {
                        double ratio = tile.getAspectRatio();
                        if (ratio < 1.0)
                                ratio = 1.0 / ratio;
                        if (ratio < maxAspectRatio){
                                if (ignoreSize || maxNodes >= LARGE_MAX_NODES || checkSize(tile))
                                        addAndReturn = true;
                        }
                } else if (tile.width < 2 && tile.height < 2) {
                        return null;
                }
                if (tile.outsidePolygon()){
                        return new Solution(maxNodes);
                }
                if (addAndReturn){
                        double outsidePolygonRatio = tile.calcOutsidePolygonRatio();
                        if (outsidePolygonRatio > maxOutsidePolygonRatio ){
                                return null;
                        }
                        Solution solution = new Solution(maxNodes);
                        solution.add(tile);  // can't split further
                        return solution;
                }
                if (tile.getCount() < minNodes * 2){
                        return null;
                }
                Solution cached = searchGoodSolutions(tile);
                if (cached != null){
                        return cached;
                }
                // we have to split the tile
                Integer alreadyDone = null;
                if (countBad == 0 && incomplete.size() > 0){
                        alreadyDone = incomplete.remove(tile);
                        if (alreadyDone == null)
                                incomplete.clear(); // rest is not useful
                }
               
                if (alreadyDone == null && depth > 0 && tile.width * tile.height > 100){
                        if (knownBad.contains(tile))
                                return null;
                }

                // copy the existing density info from parent
                // typically, at least one half can be re-used
                TileMetaInfo smi = new TileMetaInfo(tile, parent, smiParent);
               
                // we have to split the tile
                int axis = (tile.getAspectRatio() >= 1.0) ? AXIS_HOR:AXIS_VERT;
               
                IntArrayList todoList = generateTestCases(axis, tile, smi);
                int countAxis = 0;
                int usedTestPos = 0;
                int countDone = 0;
                Solution bestSol = null;
                while(true){
                        if (usedTestPos >= todoList.size()){
                                countAxis++;
                                if (countAxis > 1)
                                        break;
                                axis = axis == AXIS_HOR ? AXIS_VERT : AXIS_HOR;
                                todoList = generateTestCases(axis, tile, smi);
                                usedTestPos = 0;
                                continue;
                        }
                        countDone++;
                        if (alreadyDone != null && countDone <= alreadyDone.intValue()){
                                continue;
                        }
                        int splitPos = todoList.getInt(usedTestPos++);
                        // create the two parts of the tile
                        boolean ok = false;
                        if (axis == AXIS_HOR){
                                ok = tile.splitHoriz(splitPos, smi);
                        } else {
                                ok = tile.splitVert(splitPos, smi);
                        }
                        if (!ok)
                                continue;

                        Tile[] parts = smi.getParts();
                        if (trimTiles){
                                parts[0] = parts[0].trim();
                                parts[1] = parts[1].trim();
                        }
                        if (parts[0].getCount() > parts[1].getCount()){
                                // first try the less populated part
                                Tile help = parts[0];
                                parts[0] = parts[1];
                                parts[1] = help;
                        }
                        Solution [] sols = new Solution[2];
                        int countOK = 0;
                        for (int i = 0; i < 2; i++){
                                // depth first recursive search
//                              long t1 = System.currentTimeMillis();
                                sols[i] = findSolution(depth + 1, parts[i], tile, smi);
//                              long dt = System.currentTimeMillis() - t1;
//                              if (dt > 100 && tile.getCount() < 8 * maxNodes)
//                                      System.out.println("took " + dt + " ms to find " + sols[i] + " for "+ parts[i]);
                                if (sols[i] == null){
                                        countBad++;
//                                      if (countBad >= searchLimit){
//                                              if (countBad == searchLimit)
//                                                      System.out.println("giving up at depth " + depth + " with currX = " + currX + " and currY = "+ currY + " at tile " + tile + " bad: " + saveCountBad );
//                                              else
//                                                      System.out.println(".. " + depth + " with currX = " + currX + " and currY = "+ currY + " at tile " + tile + " bad: " + saveCountBad );
//                                      }
                                        break;
                                }
                                checkIfGood(parts[i], sols[i]);
                                countOK++;
                        }
                        if (countOK == 2){
                                Solution sol = sols[0];
                                sol.merge(sols[1]);
                                bestSol = sol;
                                break; // we found a valid split and searched the direct neighbours
                        }
                        if (countBad >= searchLimit){
                                incomplete.put(tile, countDone-1);
                                break;
                        }
                }
               
                smi.propagateToParent(smiParent, tile, parent);
                       
                if (bestSol == null && countBad < searchLimit && depth > 0 && tile.width * tile.height > 100){
                        knownBad.add(tile);
                }
                return bestSol;
        }
       
        private boolean checkSize(Tile tile) {
                return tile.height <= maxTileHeight && tile.width <= maxTileWidth;
        }
       
        /**
         * Get a first solution and search for better ones until
         * either a nice solution is found or no improvement was
         * found.
         * @param startTile the tile to split
         * @return a solution (maybe be empty)
         */

        private Solution solveRectangularArea(Tile startTile){
                // start values for optimization process: we make little steps towards a good solution
//              spread = 7;
            if (startTile.getCount() == 0)
                return new Solution(maxNodes);
                searchLimit = startSearchLimit;
                minNodes = Math.max(Math.min((long)(0.05 * maxNodes), extraDensityInfo.getNodeCount()), 1);

                if (extraDensityInfo.getNodeCount() / maxNodes < 4){
                        maxAspectRatio = 32;
                } else {
                        maxAspectRatio = startTile.getAspectRatio();
                        if (maxAspectRatio < 1)
                                maxAspectRatio = 1 / maxAspectRatio;
                        if (maxAspectRatio < NICE_MAX_ASPECT_RATIO)
                                maxAspectRatio = NICE_MAX_ASPECT_RATIO;
                }
                goodSolutions = new HashMap<>(GOOD_SOL_INIT_SIZE);
                goodRatio = 0.5;
                TileMetaInfo smiStart = new TileMetaInfo(startTile, null, null);
                if (startTile.getCount() < 300 * maxNodes && (checkSize(startTile) || startTile.getCount() < 10 * maxNodes) ){
                        searchAll = true;
                }
               
                if (!beQuiet)
                        System.out.println("Trying to find nice split for " + startTile);
                Solution bestSolution = new Solution(maxNodes);
                Solution prevBest = new Solution(maxNodes);
                long t1 = System.currentTimeMillis();
                incomplete = new LinkedHashMap<>();
                resetCaches();
                for (int numLoops = 0; numLoops < MAX_LOOPS; numLoops++){
                        double saveMaxAspectRatio = maxAspectRatio;
                        long saveMinNodes = minNodes;
                        boolean foundBetter = false;
                        Solution solution = null;
                        countBad = 0;
                        if (!beQuiet){
                                System.out.println("searching for split with min-nodes " + minNodes + ", learned " + goodSolutions.size() + " good partial solutions");
                        }
                        smiStart.setMinNodes(minNodes);
                        solution = findSolution(0, startTile, startTile, smiStart);
                        if (solution != null){
                                foundBetter = bestSolution.compareTo(solution) > 0;
                                if (foundBetter){
                                        prevBest = bestSolution;
                                        bestSolution = solution;
                                       
                                        System.out.println("Best solution until now: " + bestSolution.toString() + ", elapsed search time: " + (System.currentTimeMillis() - t1) / 1000 + " s");
                                        filterGoodSolutions(bestSolution);
                                        // change criteria to find a better(nicer) result
                                        double factor = 1.10;
                                        if (prevBest.isEmpty() == false && prevBest.isNice() )
                                                factor = Math.min(1.30,(double)bestSolution.getWorstMinNodes() / prevBest.getWorstMinNodes());
                                        minNodes = Math.max(maxNodes /3, (long) (bestSolution.getWorstMinNodes() * factor));
                                }
                                if (bestSolution.size() == 1){
                                        if (!beQuiet)
                                                System.out.println("This can't be improved.");
                                        break;
                                }
                        }
                        else {
                                if (bestSolution.isEmpty() == false){
                                        if (minNodes > bestSolution.getWorstMinNodes() + 1){
                                                // reduce minNodes
                                                minNodes = (bestSolution.getWorstMinNodes() + minNodes) / 2;
                                                if (minNodes < bestSolution.getWorstMinNodes() * 1.001)
                                                        minNodes = bestSolution.getWorstMinNodes() + 1;
                                        }
                                }
                        }
                        maxAspectRatio = Math.max(bestSolution.getWorstAspectRatio()/2, NICE_MAX_ASPECT_RATIO);
                        maxAspectRatio = Math.min(32,maxAspectRatio);
                        if (bestSolution.isEmpty() == false && bestSolution.getWorstMinNodes() > VERY_NICE_FILL_RATIO * maxNodes)
                                break;
                        if (minNodes > VERY_NICE_FILL_RATIO * maxNodes)
                                minNodes = (long) (VERY_NICE_FILL_RATIO * maxNodes);
                        if (saveMaxAspectRatio == maxAspectRatio && saveMinNodes == minNodes){
                                if (bestSolution.isEmpty() || bestSolution.getWorstMinNodes() < 0.5 * maxNodes){
                                        if (countBad > searchLimit && searchLimit < 5_000_000){
                                                searchLimit *= 2;
                                                resetCaches();
                                                System.out.println("No good solution found, duplicated search-limit to " + searchLimit);
                                                continue;
                                        }
                                        if (bestSolution.isEmpty() && minNodes > 1){
                                                minNodes = 1;
                                                resetCaches();
                                                searchLimit = startSearchLimit;
                                                // sanity check
                                                System.out.println("No good solution found, trying to find one accepting anything");
                                                int highestCount = extraDensityInfo.getMaxNodesInDensityMapGridElement();
                                                // inform user about possible better options?
                                                double ratio = (double) highestCount / maxNodes;
                                                if (ratio > 4)
                                                        System.err.printf("max-nodes value %d is far below highest node count %d in single grid element, consider using a higher resolution.\n", maxNodes , highestCount);
                                                else if (ratio > 1)
                                                        System.err.printf("max-nodes value %d is below highest node count %d in single grid element, consider using a higher resolution.\n", maxNodes , highestCount);
                                                else if (ratio < 0.25)
                                                        System.err.printf("max-nodes value %d is far above highest node count %d in single grid element, consider using a lower resolution.\n", maxNodes , highestCount);
                                               
                                                continue;
                                        }
                                        if (searchAll){
                                                searchAll = false;
                                                if (bestSolution.isEmpty() == false)
                                                        minNodes = bestSolution.getWorstMinNodes() + 1;
                                                else
                                                        minNodes = maxNodes / 100;
                                                System.out.println("Still no good solution found, trying alternate algorithm");
                                                continue;
                                        }
                                }  
                                break;
                        }

                }
                if (!beQuiet)
                        printFinishMsg(bestSolution);
                return bestSolution;
        }

        private void resetCaches(){
                knownBad = new HashSet<>(50_000);
        }
       
        private void printFinishMsg(Solution solution){
                if (!beQuiet){
                        if (solution.isEmpty() == false){
                                if (solution.getWorstMinNodes() > VERY_NICE_FILL_RATIO * maxNodes && solution.isNice())
                                        System.out.println("Solution is very nice. No need to search for a better solution: " + solution.toString());
                                else
                                        System.out.println("Solution is " + (solution.isNice() ? "":"not ") + "nice. Can't find a better solution with search-limit " + searchLimit + ": " + solution.toString());
                        }
                }
                return;
        }
       
        /**
         * Convert the list of Tile instances of a solution to Area instances, report some
         * statistics.
         * @param sol the solution
         * @param polygonArea
         *
         * @return list of areas
         */

        private List<Area> getAreas(Solution sol, java.awt.geom.Area polygonArea) {
                List<Area> result = new ArrayList<>();
                int minLat = getAllDensities().getBounds().getMinLat();
                int minLon = getAllDensities().getBounds().getMinLong();
               
                if (polygonArea != null){
                        System.out.println("Trying to cut the areas so that they fit into the polygon ...");
                } else {
                        if (trimShape)
                                sol.trimOuterTiles();
                }
               
                boolean fits  = true;
                for (Tile tile : sol.getTiles()) {
                        if (tile.getCount() == 0)
                                continue;
                        if (tile.verify() == false)
                                throw new SplitFailedException("found invalid tile");
                        Rectangle r = new Rectangle(minLon + (tile.x << shift), minLat + (tile.y << shift),
                                        tile.width << shift, tile.height << shift);
                       
                        if (polygonArea != null){
                                java.awt.geom.Area cutArea = new java.awt.geom.Area(r);
                                cutArea.intersect(polygonArea);
                                if (cutArea.isEmpty() == false && cutArea.isRectangular() )
                                        r = cutArea.getBounds();
                                else {
                                        fits = false;
                                }
                        }
                        Area area = new Area(r.y,r.x,(int)r.getMaxY(),(int)r.getMaxX());
                        if (!beQuiet){
                                String note;
                                if (tile.getCount() > maxNodes)
                                        note = " but is already at the minimum size so can't be split further";
                                else
                                        note = "";
                                long percentage = 100 * tile.getCount() / maxNodes;
                                System.out.println("Area " + currMapId++ + " covers " + area
                                                + " and contains " + tile.getCount() + " nodes (" + percentage + " %)" + note);
                        }
                        result.add(area);
                }
                if (fits == false){
                        System.out.println("One or more areas do not exactly fit into the bounding polygon");
                }
                return result;

        }

        /**
         * Generate a list of split positions.
         * This is essential: The shorter the list, the faster the search, so we try to
         * put only good candidates into the list.
         * @param axis
         * @param tile
         * @param smi
         * @return
         */

        private IntArrayList generateTestCases(int axis, Tile tile, TileMetaInfo smi) {
                if (searchAll){
                        return (axis == AXIS_HOR) ? tile.genXTests(smi) : tile.genYTests(smi);
                }
                double ratio = tile.getAspectRatio();
                IntArrayList tests = new IntArrayList();
                if (ratio < 1.0 / 32 || ratio > 32 || ratio < 1.0 / 16 && axis == AXIS_HOR || ratio > 16 && axis == AXIS_VERT)
                        return tests;
                int start = (axis == AXIS_HOR) ? tile.findValidStartX(smi) : tile.findValidStartY(smi);
                int end = (axis == AXIS_HOR) ? tile.findValidEndX(smi) : tile.findValidEndY(smi);
                int range = end - start;
                if (range < 0){
                        // can't split tile without having one part that has too few nodes
                        return tests;
                }
                if (range > 1024 && (axis == AXIS_HOR && tile.width >= maxTileWidth || axis == AXIS_VERT && tile.height >= maxTileWidth)){
                        // large tile, just split at a few valid positions
                        for (int i = 5; i > 1; --i)
                                tests.add(start + range / i);
                }
                else if (tile.getCount() < maxNodes * 4 && range > 256){
                        // large tile with rather few nodes, allow more tests
                        int step = (range) / 20;
                        for (int pos = start; pos <= end; pos += step)
                                tests.add(pos);
                }
                else if (tile.getCount() > maxNodes * 4){
                        int step = range / 7; // 7 turned out to be a good value here
                        if (step < 1)
                                step = 1;
                        for (int pos = start; pos <= end; pos += step)
                                tests.add(pos);
                } else {
                        // this will be one of the last splits
                        long nMax = tile.getCount() / minNodes;
                        if (nMax * minNodes < tile.getCount())
                                nMax++;
                        long nMin = tile.getCount() / maxNodes;
                        if (nMin * maxNodes < tile.getCount())
                                nMin++;
                        if (nMin > 2 && nMin * maxNodes - minNodes < tile.getCount() && ratio > 0.125 && ratio < 8){
                                // count is near (but below) a multiple of max-nodes, we have to test all candidates  
                                // to make sure that we don't miss a good split
                                return (axis == AXIS_HOR) ? tile.genXTests(smi) : tile.genYTests(smi);
                        }
                        if (nMax == 2 || nMin == 2){
                                tests.add((axis == AXIS_HOR) ? tile.findHorizontalMiddle(smi) : tile.findVerticalMiddle(smi));
                                int pos = (axis == AXIS_HOR) ? tile.findFirstXHigher(smi, minNodes) + 1 : tile.findFirstYHigher(smi, minNodes) + 1;
                                if (tests.get(0) != pos)
                                        tests.add(pos);
                                pos = (axis == AXIS_HOR) ? tile.findFirstXHigher(smi, maxNodes) : tile.findFirstYHigher(smi, maxNodes);
                                if (tests.contains(pos) == false)
                                        tests.add(pos);
                        } else {
                                if (range == 0) {
                                        tests.add(start);
                                } else {
                                        if (nMax != 3)
                                                tests.add((axis == AXIS_HOR) ? tile.findHorizontalMiddle(smi) : tile.findVerticalMiddle(smi));
                                        if (tests.contains(start) == false)
                                                tests.add(start);
                                        if (tests.contains(end) == false)
                                                tests.add(end);
                                }
                        }
                }
                return tests;
        }
}