Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2007 Steve Ratcliffe
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License 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.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: 20-Jan-2007
 */

package uk.me.parabola.mkgmap.build;

import java.util.ArrayList;
import java.util.List;

import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.trergn.Zoom;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.MapDataSource;

/**
 * The map must be split into subdivisions.  To do this we start off with
 * one of these MapAreas containing all of the map and split it up into
 * smaller and smaller areas until each area is below a maximum size and
 * contains fewer than a maximum number of map features.
 *
 * @author Steve Ratcliffe
 */

public class MapSplitter {
        private static final Logger log = Logger.getLogger(MapSplitter.class);

        private final MapDataSource mapSource;

        // There is an absolute largest size as offsets are in 16 bits, but
        // the top bit of height is probably a flag for GMP "RoadRef" data.  
        public static final int MAX_DIVISION_SIZE = 0x7fff;

        // Not a real limit.  Note that the offset to the start of a section
        // has to fit into 16 bits, the end of the last section could be beyond the
        // 16 bit limit. Leave a little room for the region pointers
        public static final int MAX_RGN_OFFSET_SIZE = 0xfff8;

        // The maximum number of lines. NET points to lines in subdivision
        // using bytes.
        public static final int MAX_NUM_LINES = 0xff;

        public static final int MAX_NUM_POINTS = 0xff;

        // maximum allowed amounts of points/lines/shapes with extended types
        // real limits are not known but if these values are too large, data
        // goes missing (lines disappear, etc.)
        public static final int MAX_XT_POINTS_SIZE = 0xff00;
        public static final int MAX_XT_LINES_SIZE  = 0xff00;
        public static final int MAX_XT_SHAPES_SIZE = 0xff00;
       
        public static final int MIN_DIMENSION = 10; // just a reasonable value

        // The target number of estimated bytes for one area, smaller values
        // result in more and typically smaller areas and larger *.img files
        public static final int WANTED_MAX_AREA_SIZE = 0x3fff;
       
        private final Zoom zoom;

        /**
         * Creates a list of map areas and keeps splitting them down until they
         * are small enough.  There is both a maximum size to an area and also
         * a maximum number of things that will fit inside each division.
         *
         * Since these are not well defined (it all depends on how complicated the
         * features are etc), we shall underestimate the maximum sizes and probably
         * make them configurable.
         *
         * @param mapSource The input map data source.
         * @param zoom The zoom level that we need to split for.
         */

        MapSplitter(MapDataSource mapSource, Zoom zoom) {
                this.mapSource = mapSource;
                this.zoom = zoom;
        }

        /**
         * This splits the map into a series of smaller areas.  There is both a
         * maximum size and a maximum number of features that can be contained
         * in a single area.
         *
         * This routine is not called recursively.
         *
         * @param orderByDecreasingArea aligns subareas as powerOf2 and splits polygons into the subareas.  
         * @return An array of map areas, each of which is within the size limit
         * and the limit on the number of features.
         */

        public MapArea[] split(boolean orderByDecreasingArea) {
                log.debug("orig area", mapSource.getBounds());

                MapArea ma = initialArea(mapSource, orderByDecreasingArea);
                MapArea[] origArea = {ma};
                MapArea[] areas = splitMaxSize(ma);
                if (areas == null) {
                        log.warn("initial split returned null for", ma);
                        return origArea;
                }

                // Now step through each area and see if any have too many map features
                // in them.  For those that do, we further split them.  This is done
                // recursively until everything fits.
                List<MapArea> alist = new ArrayList<>();
                addAreasToList(areas, alist, 0);
                if (alist.isEmpty()) {
                        return origArea;
                }

                MapArea[] results = new MapArea[alist.size()];
                return alist.toArray(results);
        }

        /**
         * Adds map areas to a list.  If an area has too many features, then it
         * is split into 2 and this routine is called recursively to add the new
         * areas.
         *
         * @param areas The areas to add to the list (and possibly split up).
         * @param alist The list that will finally contain the complete list of
         * map areas.
         */

        private void addAreasToList(MapArea[] areas, List<MapArea> alist, int depth) {
                int shift = zoom.getShiftValue();

                for (MapArea area : areas) {
                        if (!area.hasData())
                                continue;
                        Area bounds = area.getBounds();
                        int[] sizes = area.getEstimatedSizes();
                        if(log.isInfoEnabled()) {
                                String padding = depth + "                                                                      ";
                                log.info(padding.substring(0, (depth + 1) * 2) +
                                                 bounds.getWidth() + "x" + bounds.getHeight() +
                                                 ", points = " + area.getNumPoints() + "/" + sizes[MapArea.POINT_KIND] + "/" + sizes[MapArea.XT_POINT_KIND] +
                                                 ", lines = " + area.getNumLines() + "/" + sizes[MapArea.LINE_KIND] + "/" + sizes[MapArea.XT_LINE_KIND] +
                                                 ", shapes = " + area.getNumShapes() + "/" + sizes[MapArea.SHAPE_KIND] + "/" + sizes[MapArea.XT_SHAPE_KIND]);
                        }

                        boolean wantSplit = false;
                        boolean mustSplit = false;
                        if (area.getNumLines() > MAX_NUM_LINES || area.getNumPoints() > MAX_NUM_POINTS
                                        || (sizes[MapArea.POINT_KIND] + sizes[MapArea.LINE_KIND]) > MAX_RGN_OFFSET_SIZE
                                        || sizes[MapArea.XT_POINT_KIND] > MAX_XT_POINTS_SIZE
                                        || sizes[MapArea.XT_LINE_KIND] > MAX_XT_LINES_SIZE
                                        || sizes[MapArea.XT_SHAPE_KIND] > MAX_XT_SHAPES_SIZE)
                                mustSplit = true;
                        else if (bounds.getMaxDimension() > (MIN_DIMENSION << shift)) {
                                int sumSize = 0;
                                for (int s : sizes)
                                        sumSize += s;
                                if (sumSize > WANTED_MAX_AREA_SIZE) {
                                        // area has more bytes than wanted, and is large enough to split
                                        log.debug("splitting area because estimated data size is larger than wanted:", sumSize);
                                        wantSplit = true;
                                }
                        }

                        if (wantSplit || mustSplit) {
                                if (!area.canSplit()) {
                                        if (!mustSplit) {
                                                log.info("Single item larger that WANTED_MAX_AREA_SIZE", area.getBounds().getCenter().toOSMURL());
                                        }
                                } else if (bounds.getMaxDimension() > (MIN_DIMENSION << shift)) {
                                        log.debug("splitting area in half", area, mustSplit, wantSplit);
                                        MapArea[] sublist;
                                        if (bounds.getWidth() > bounds.getHeight())
                                                sublist = area.split(2, 1, bounds, false);
                                        else
                                                sublist = area.split(1, 2, bounds, false);
                                        if (sublist == null)
                                                log.error("SubDivision split failed at", area.getBounds().getCenter().toOSMURL());
                                        else {
                                                addAreasToList(sublist, alist, depth + 1);
                                                continue;
                                        }
                                } else if (mustSplit) { // can't reduce size, so force more subdivisions
                                        log.debug("splitting area by contents", area);
                                        MapArea[] sublist = area.split(1, 1, bounds, true);
                                        addAreasToList(sublist, alist, depth + 1);
                                        continue;
                                }
                        }

                        log.debug("adding area unsplit: has points", area.hasPoints());
                        alist.add(area);
                }
        } // addAreasToList

   
        /**
         * Split the area into portions that have the maximum size.  There is a
         * maximum limit to the size of a subdivision (16 bits or about 1.4 degrees
         * at the most detailed zoom level).
         *
         * The size depends on the shift level.
         *
         * We are choosing a limit smaller than the real max to allow for
         * uncertainty about what happens with features that extend beyond the box.
         *
         * If the area is already small enough then it will be returned unchanged.
         *
         * @param mapArea The area that needs to be split down.
         * @return An array of map areas.  Each will be below the max size.
         */

        private MapArea[] splitMaxSize(MapArea mapArea) {
                /**
                 * mapArea.getBounds() comes from the original map source or parent/split MapArea.
                 * mapArea.getFullBounds() is calculated from elements added to the MapArea.
                 * Normally, mapArea.getBounds() and getFullBounds() are well defined and
                 * getFullBounds() is a little bit bigger than bounds() because lines/shapes are allowed
                 * to go slightly out of their area.
                 * MapArea.split() should use getBounds() because otherwise can get primary area overlap
                 * which conflicts with concept of orderByDecreasingArea.
                 * Some map sources might not set getBounds().
                 * If there are no elements, getFullBounds isEmpty.
                */

                int shift = zoom.getShiftValue();
                Area bounds = mapArea.getBounds();
                if (bounds.isEmpty() || (!mapArea.getFullBounds().isEmpty()
                                && (mapArea.getFullBounds().getMaxDimension() >> shift) > MAX_DIVISION_SIZE))
                        bounds = mapArea.getFullBounds();
                if (bounds.isEmpty())
                        return null;

                int width = bounds.getWidth() >> shift;
                int height = bounds.getHeight() >> shift;

                // There is an absolute maximum size that a division can be.  Make sure
                // that we are well inside that.
                int xsplit = 1;
                if (width > MAX_DIVISION_SIZE)
                        xsplit = width / MAX_DIVISION_SIZE + 1;

                int ysplit = 1;
                if (height > MAX_DIVISION_SIZE)
                        ysplit = height / MAX_DIVISION_SIZE + 1;

                log.debug("splitMaxSize: bounds", bounds, "shift", shift, "width", width, "height", height, "xsplit", xsplit, "ysplit", ysplit);
                return mapArea.split(xsplit, ysplit, bounds, false);
        }

        /**
         * The initial area contains all the features of the map.
         *
         * @param src The map data source.
         * @param splitPolygonsIntoArea aligns subareas as powerOf2 and splits polygons into the subareas.  
         * @return The initial map area covering the whole area and containing
         * all the map features that are visible.
         */

        private MapArea initialArea(MapDataSource src, boolean splitPolygonsIntoArea) {
                return new MapArea(src, zoom.getResolution(), splitPolygonsIntoArea);
        }
}