/* * 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.Collections; 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, we are // staying safely inside it however. public static final int MAX_DIVISION_SIZE = 0x7fff; // The maximum region size. 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_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 private 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); MapArea[] origArea = {ma}; MapArea[] areas = splitMaxSize(ma, orderByDecreasingArea); 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, orderByDecreasingArea); 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 4 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. * @param orderByDecreasingArea aligns subareas as powerOf2 and splits polygons into the subareas. */ private void addAreasToList(MapArea[] areas, List<MapArea> alist, int depth, boolean orderByDecreasingArea) { int res = zoom.getResolution(); for (MapArea area : areas) { Area bounds = area.getBounds(); int[] sizes = area.getEstimatedSizes(); if (area.hasData() == false) continue; if(log.isInfoEnabled()) { String padding = depth + " "; log.info(padding.substring(0, (depth + 1) * 2) + bounds.getWidth() + "x" + bounds.getHeight() + ", res = " + res + ", points = " + area.getNumPoints() + "/" + sizes[MapArea.POINT_KIND] + ", lines = " + area.getNumLines() + "/" + sizes[MapArea.LINE_KIND] + ", shapes = " + area.getNumShapes() + "/" + sizes[MapArea.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] + sizes[MapArea.SHAPE_KIND]) > MAX_RGN_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; // we must split else if (bounds.getMaxDimension() > MIN_DIMENSION) { int sumSize = 0; for (int s : sizes) sumSize += s; if (sumSize > WANTED_MAX_AREA_SIZE) { if (area.getLines().size() + area.getShapes().size() >= 2) { // area has more bytes than wanted, and we can split log.debug("splitting area because size is larger than wanted: " + sumSize); wantSplit = true; } } } if (wantSplit || mustSplit){ if (bounds.getMaxDimension() > MIN_DIMENSION) { if (log.isDebugEnabled()) log.debug("splitting area", area); MapArea[] sublist; if(bounds.getWidth() > bounds.getHeight()) sublist = area.split(2, 1, res, bounds, orderByDecreasingArea); else sublist = area.split(1, 2, res, bounds, orderByDecreasingArea); if (sublist == null) { String msg = "SubDivision is single point at this resolution so can't split at " + area.getBounds().getCenter().toOSMURL(); if (wantSplit) { log.info(msg + " (probably harmless)"); } else { log.error(msg); } } else { addAreasToList(sublist, alist, depth + 1, orderByDecreasingArea); continue; } } else { log.error("Area too small to split at " + area.getBounds().getCenter().toOSMURL() + " (reduce the density of points, length of lines, etc.)"); } } log.debug("adding area unsplit", ",has points" + area.hasPoints()); alist.add(area); } } /** * 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. * @param orderByDecreasingArea aligns subareas as powerOf2 and splits polygons into the subareas. * @return An array of map areas. Each will be below the max size. */ private MapArea[] splitMaxSize(MapArea mapArea, boolean orderByDecreasingArea) { Area bounds = mapArea.getFullBounds(); int shift = zoom.getShiftValue(); int width = bounds.getWidth() >> shift; int height = bounds.getHeight() >> shift; log.info("splitMaxSize() bounds = " + bounds + " shift = " + shift + " width = " + width + " height = " + height); if (log.isDebugEnabled()) log.debug("shifted width", width, "shifted height", height); // 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; return mapArea.split(xsplit, ysplit, zoom.getResolution(), bounds, orderByDecreasingArea); } /** * The initial area contains all the features of the map. * * @param src The map data source. * @return The initial map area covering the whole area and containing * all the map features that are visible. */ private MapArea initialArea(MapDataSource src) { return new MapArea(src, zoom.getResolution()); } }