Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2007 - 2012.
 *
 * 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.build;

import java.awt.Rectangle;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.MapTooBigException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.Exit;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.dem.DEMFile;
import uk.me.parabola.imgfmt.app.lbl.City;
import uk.me.parabola.imgfmt.app.lbl.Country;
import uk.me.parabola.imgfmt.app.lbl.ExitFacility;
import uk.me.parabola.imgfmt.app.lbl.Highway;
import uk.me.parabola.imgfmt.app.lbl.LBLFile;
import uk.me.parabola.imgfmt.app.lbl.POIRecord;
import uk.me.parabola.imgfmt.app.lbl.Region;
import uk.me.parabola.imgfmt.app.lbl.Zip;
import uk.me.parabola.imgfmt.app.map.Map;
import uk.me.parabola.imgfmt.app.net.NETFile;
import uk.me.parabola.imgfmt.app.net.NODFile;
import uk.me.parabola.imgfmt.app.net.Numbers;
import uk.me.parabola.imgfmt.app.net.RoadDef;
import uk.me.parabola.imgfmt.app.net.RoadNetwork;
import uk.me.parabola.imgfmt.app.net.RouteCenter;
import uk.me.parabola.imgfmt.app.trergn.ExtTypeAttributes;
import uk.me.parabola.imgfmt.app.trergn.Overview;
import uk.me.parabola.imgfmt.app.trergn.Point;
import uk.me.parabola.imgfmt.app.trergn.PointOverview;
import uk.me.parabola.imgfmt.app.trergn.Polygon;
import uk.me.parabola.imgfmt.app.trergn.PolygonOverview;
import uk.me.parabola.imgfmt.app.trergn.Polyline;
import uk.me.parabola.imgfmt.app.trergn.PolylineOverview;
import uk.me.parabola.imgfmt.app.trergn.RGNFile;
import uk.me.parabola.imgfmt.app.trergn.RGNHeader;
import uk.me.parabola.imgfmt.app.trergn.Subdivision;
import uk.me.parabola.imgfmt.app.trergn.TREFile;
import uk.me.parabola.imgfmt.app.trergn.TREHeader;
import uk.me.parabola.imgfmt.app.trergn.Zoom;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.CommandArgs;
import uk.me.parabola.mkgmap.Version;
import uk.me.parabola.mkgmap.filters.BaseFilter;
import uk.me.parabola.mkgmap.filters.DouglasPeuckerFilter;
import uk.me.parabola.mkgmap.filters.FilterConfig;
import uk.me.parabola.mkgmap.filters.LineMergeFilter;
import uk.me.parabola.mkgmap.filters.LinePreparerFilter;
import uk.me.parabola.mkgmap.filters.LineSplitterFilter;
import uk.me.parabola.mkgmap.filters.MapFilter;
import uk.me.parabola.mkgmap.filters.MapFilterChain;
import uk.me.parabola.mkgmap.filters.PolygonSplitterFilter;
import uk.me.parabola.mkgmap.filters.RemoveEmpty;
import uk.me.parabola.mkgmap.filters.RemoveObsoletePointsFilter;
import uk.me.parabola.mkgmap.filters.RoundCoordsFilter;
import uk.me.parabola.mkgmap.filters.ShapeMergeFilter;
import uk.me.parabola.mkgmap.filters.ShapeMergeFilter.MapShapeComparator;
import uk.me.parabola.mkgmap.filters.SizeFilter;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.LoadableMapDataSource;
import uk.me.parabola.mkgmap.general.MapDataSource;
import uk.me.parabola.mkgmap.general.MapElement;
import uk.me.parabola.mkgmap.general.MapExitPoint;
import uk.me.parabola.mkgmap.general.MapLine;
import uk.me.parabola.mkgmap.general.MapPoint;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.general.ZipCodeInfo;
import uk.me.parabola.mkgmap.reader.MapperBasedMapDataSource;
import uk.me.parabola.mkgmap.reader.hgt.HGTConverter;
import uk.me.parabola.mkgmap.reader.hgt.HGTConverter.InterpolationMethod;
import uk.me.parabola.mkgmap.reader.hgt.HGTReader;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.GeneralRelation;
import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.mkgmap.reader.overview.OverviewMapDataSource;
import uk.me.parabola.util.Configurable;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.Java2DConverter;
import uk.me.parabola.util.ShapeSplitter;

/**
 * This is the core of the code to translate from the general representation
 * into the garmin representation.
 *
 * We need to go through the data several times, once for each level, filter
 * out features that are not required at the level and simplify paths for
 * lower resolutions if required.
 *
 * @author Steve Ratcliffe
 */

public class MapBuilder implements Configurable {
        private static final Logger log = Logger.getLogger(MapBuilder.class);
        private static final int CLEAR_TOP_BITS = (32 - 15);
        private static final LocalDateTime now = LocalDateTime.now();
       
        private static final int MIN_SIZE_LINE = 1;

        private final boolean isOverviewComponent;
        private final boolean isOverviewCombined;

        private final java.util.Map<MapPoint,POIRecord> poimap = new HashMap<>();
        private final java.util.Map<MapPoint,City> cityMap = new HashMap<>();
        private List<String> mapInfo = new ArrayList<>();
        private List<String> copyrights = new ArrayList<>();

        private boolean hasNet;
        private Boolean driveOnLeft; // needs to be Boolean for later null test
        private Locator locator;

        private final java.util.Map<String, Highway> highways = new HashMap<>();

        /** name that is used for cities which name are unknown */
        private static final String UNKNOWN_CITY_NAME = "";

        private Country defaultCountry;
        private String countryName = "COUNTRY";
        private String countryAbbr = "ABC";
        private String regionName;
        private String regionAbbr;
       
        private Set<String> locationAutofill;

        private int minSizePolygon;
        private String polygonSizeLimitsOpt;
        private TreeMap<Integer,Integer> polygonSizeLimits;
        private TreeMap<Integer, Double> dpFilterLineResMap;
        private TreeMap<Integer, Double> dpFilterShapeResMap;
        private boolean mergeLines;
        private boolean mergeShapes;

        private boolean poiAddresses;
        private int             poiDisplayFlags;
        private boolean enableLineCleanFilters = true;
        private boolean makePOIIndex;
        private int routeCenterBoundaryType;
       
        private LBLFile lblFile;

        private String licenseFileName;

        private boolean orderByDecreasingArea;
        private String pathsToHGT;
        private List<Integer> demDists;
        private short demOutsidePolygonHeight;
        private java.awt.geom.Area demPolygon;
        private HGTConverter.InterpolationMethod demInterpolationMethod;
        private boolean allowReverseMerge;
        private boolean improveOverview;

        /**
         * Construct a new MapBuilder.
         *
         * @param overviewComponent set to {@code true} if the map is a work file that
         *                          is later used as input for the OverviewBuilder
         * @param overviewCombined  set to {@code true} if the map is the combined
         *                          overview map
         */

        public MapBuilder(boolean overviewComponent, boolean overviewCombined) {
                regionName = null;
                locationAutofill = Collections.emptySet();
                locator = new Locator();
                this.isOverviewComponent = overviewComponent;
                this.isOverviewCombined = overviewCombined;
        }

        public void config(EnhancedProperties props) {

                countryName = props.getProperty("country-name", countryName);
                countryAbbr = props.getProperty("country-abbr", countryAbbr);
                regionName = props.getProperty("region-name", null);
                regionAbbr = props.getProperty("region-abbr", null);
                minSizePolygon = props.getProperty("min-size-polygon", 8);
                polygonSizeLimitsOpt = props.getProperty("polygon-size-limits", null);
                // options for DouglasPeuckerFilter
                double reducePointError = props.getProperty("reduce-point-density", 2.6);
                double reducePointErrorPolygon = props.getProperty("reduce-point-density-polygon", -1);
                if (reducePointErrorPolygon == -1)
                        reducePointErrorPolygon = reducePointError;
                dpFilterLineResMap = parseLevelOption(props, "simplify-lines", reducePointError);
                dpFilterShapeResMap = parseLevelOption(props, "simplify-polygons", reducePointErrorPolygon);
               
                mergeLines = props.containsKey("merge-lines");
                allowReverseMerge = props.getProperty("allow-reverse-merge", false);

                // undocumented option - usually used for debugging only
                mergeShapes = !props.getProperty("no-mergeshapes", false);
                improveOverview = props.getProperty("improve-overview", false);
               
                makePOIIndex = props.getProperty("make-poi-index", false);

                if(props.getProperty("poi-address") != null)
                        poiAddresses = true;

                routeCenterBoundaryType = props.getProperty("route-center-boundary", 0);

                licenseFileName = props.getProperty("license-file", null);
               
                locationAutofill = LocatorUtil.parseAutofillOption(props);
               
                locator = new Locator(props);
                locator.setDefaultCountry(countryName, countryAbbr);
                String driveOn = props.getProperty("drive-on",null);
                if ("left".equals(driveOn))
                        driveOnLeft = true;
                if ("right".equals(driveOn))
                        driveOnLeft = false;
                orderByDecreasingArea = props.getProperty("order-by-decreasing-area", false);
                pathsToHGT = props.getProperty("dem", null);
                String demDistStr = props.getProperty("dem-dists", "-1");
                demOutsidePolygonHeight = (short) props.getProperty("dem-outside-polygon", HGTReader.UNDEF);
                String demPolygonFile = props.getProperty("dem-poly", null);
                if (demPolygonFile != null) {
                        demPolygon = Java2DConverter.readPolyFile(demPolygonFile);
                }
                String ipm = props.getProperty("dem-interpolation", "auto");
                switch (ipm) {
                case "auto":
                        demInterpolationMethod = InterpolationMethod.AUTOMATIC;
                        break;
                case "bicubic":
                        demInterpolationMethod = InterpolationMethod.BICUBIC;
                        break;
                case "bilinear":
                        demInterpolationMethod = InterpolationMethod.BILINEAR;
                        break;
                default:
                        throw new IllegalArgumentException("invalid argument for option dem-interpolation: '" + ipm +
                                        "' supported are 'bilinear', 'bicubic', or 'auto'");
                }

                if (isOverviewCombined) { // some alternate options, some invalid etc
                        demDistStr = props.getProperty("overview-dem-dist", "-1");
                        mergeLines = true;
                        if (orderByDecreasingArea) {
                                orderByDecreasingArea = false;
                                mergeShapes = false;  // shape order in ovm_ imgs must be preserved to have the effect of above
                        } else {
                                mergeShapes = true;
                        }
                }
                demDists = parseDemDists(demDistStr);

        }

        private static List<Integer> parseDemDists(String demDists) {
                List<Integer> dists = CommandArgs.stringToList(demDists, "dem-dists")
                                .stream().map(Integer::parseInt).collect(Collectors.toList());
                if (dists.isEmpty())
                        return Arrays.asList(-1);
                return dists;
        }

        /**
         * Main method to create the map, just calls out to several routines
         * that do the work.
         *
         * @param map The map.
         * @param src The map data.
         */

        public void makeMap(Map map, LoadableMapDataSource src) {

                RGNFile rgnFile = map.getRgnFile();
                TREFile treFile = map.getTreFile();
                lblFile = map.getLblFile();
                NETFile netFile = map.getNetFile();
               
                hasNet = netFile != null;
               
                if (routeCenterBoundaryType != 0 && netFile != null && src instanceof MapperBasedMapDataSource) {
                        for (RouteCenter rc : src.getRoadNetwork().getCenters()) {
                                ((MapperBasedMapDataSource) src).addBoundaryLine(rc.getArea(), routeCenterBoundaryType,
                                                rc.reportSizes());
                        }
                }
                if (mapInfo.isEmpty())
                        getMapInfo();

                if (map.getNodFile() != null) {
                        // make sure that island detection is done before we write any map data so that NOD flags are properly set
                        src.getRoadNetwork().getCenters();
                }
                normalizeCountries(src);
               
                processCities(map, src);
                processRoads(map,src);
                processPOIs(map, src);
                processOverviews(map, src);
                processInfo(map, src);
                makeMapAreas(map, src);
                 
                if (driveOnLeft == null && src instanceof MapperBasedMapDataSource) {
                        // source can give info about driving side
                        driveOnLeft = ((MapperBasedMapDataSource) src).getDriveOnLeft();
                }
                if (driveOnLeft == null)
                        driveOnLeft = false;
                treFile.setDriveOnLeft(driveOnLeft);

                treFile.setLastRgnPos(rgnFile.position() - RGNHeader.HEADER_LEN);

                rgnFile.write();
                treFile.write(rgnFile.haveExtendedTypes());
                lblFile.write();
                lblFile.writePost();

                if (netFile != null) {
                        RoadNetwork network = src.getRoadNetwork();
                        netFile.setNetwork(network.getRoadDefs());
                        NODFile nodFile = map.getNodFile();
                        if (nodFile != null) {
                                nodFile.setNetwork(network.getCenters(), network.getRoadDefs(), network.getBoundary());
                                nodFile.setDriveOnLeft(driveOnLeft);
                                nodFile.write();
                        }
                        netFile.write(lblFile.numCities(), lblFile.numZips());

                        if (nodFile != null) {
                                nodFile.writePost();
                        }
                        netFile.writePost(rgnFile.getWriter());
                }
                warnAbout3ByteImgRefs();
                buildDem(map, src);
                treFile.writePost();
        }

        private void buildDem(Map map, LoadableMapDataSource src) {
                DEMFile demFile = map.getDemFile();
                if (demFile == null)
                        return;
                if (demDists.size() > src.mapLevels().length) {
                        throw new MapFailedException("More dem-dist values than levels: \n\t" + demDists + "\n\t"
                                        + Arrays.toString(src.mapLevels()) + "\n");
                }

                try{
                        long t1 = System.currentTimeMillis();
                        java.awt.geom.Area  demArea = null;
                        if (demPolygon != null) {
                                Area bbox = src.getBounds();
                                // the rectangle is a bit larger to avoid problems at tile boundaries
                                Rectangle2D r = new Rectangle(bbox.getMinLong() - 2, bbox.getMinLat() - 2,
                                                bbox.getWidth() + 4, bbox.getHeight() + 4);
                                if (demPolygon.intersects(r) && !demPolygon.contains(r)) {
                                        demArea = demPolygon;
                                }
                        }
                        if (demArea == null && isOverviewCombined) {
                                Path2D demPoly = ((OverviewMapDataSource) src).getTileAreaPath();
                                if (demPoly != null) {
                                        demArea = new java.awt.geom.Area(demPoly);
                                }
                        }
                        Area treArea = demFile.calc(src.getBounds(), demArea, pathsToHGT, demDists, demOutsidePolygonHeight, demInterpolationMethod);
                        map.setBounds(treArea);
                        long t2 = System.currentTimeMillis();
                        log.info("DEM file calculation for", map.getFilename(), "took", (t2 - t1), "ms");
                        demFile.write();
                } catch (MapTooBigException e) {
                        throw new MapTooBigException(e.getMaxAllowedSize(), "The DEM section of the map or tile is too big.", "Try increasing the DEM distance.");
                } catch (MapFailedException e) {
                        throw new MapFailedException("Error creating DEM File. " + e.getMessage());
                }
        }

        private void warnAbout3ByteImgRefs() {
                String mapContains = "Map contains";
                String infoMsg = "- more than 65535 might cause indexing problems and excess size. Suggest splitter with lower --max-nodes";
                int itemCount;
                itemCount = lblFile.numCities();
                if (itemCount > 0xffff)
                        log.error(mapContains, itemCount, "Cities", infoMsg);
                itemCount = lblFile.numZips();
                if (itemCount > 0xffff)
                        log.error(mapContains, itemCount, "Zips", infoMsg);
                itemCount = lblFile.numHighways();
                if (itemCount > 0xffff)
                        log.error(mapContains, itemCount, "Highways", infoMsg);
                itemCount = lblFile.numExitFacilities();
                if (itemCount > 0xffff)
                        log.error(mapContains, itemCount, "Exit facilities", infoMsg);
        } // warnAbout3ByteImgRefs

        private Country getDefaultCountry() {
                if (defaultCountry == null && lblFile != null) {
                        defaultCountry = lblFile.createCountry(countryName, countryAbbr);
                }
                return defaultCountry;
        }
       
        /**
         * Retrieves the region with the default name in the given country.
         * @param country the country ({@code null} = use default country)
         * @return the default region in the given country ({@code null} if not available)
         */

        private Region getDefaultRegion(Country country) {
                if (lblFile == null || regionName == null) {
                        return null;
                }
                if (country == null) {
                        if (getDefaultCountry() == null) {
                                return null;
                        } else {
                                return lblFile.createRegion(getDefaultCountry(), regionName, regionAbbr);
                        }
                } else {
                        return lblFile.createRegion(country, regionName, regionAbbr);
                }
        }

        /**
         * Process the country names of all elements and normalize them
         * so that one consistent country name is used for the same country
         * instead of different spellings.
         * @param src the source of elements
         */

        private void normalizeCountries(MapDataSource src) {
                for (MapPoint p : src.getPoints()) {
                        String countryStr = p.getCountry();
                        if (countryStr != null) {
                                countryStr = locator.normalizeCountry(countryStr);
                                p.setCountry(countryStr);
                        }
                }

                for (MapLine l : src.getLines()) {
                        String countryStr = l.getCountry();
                        if (countryStr != null) {
                                countryStr = locator.normalizeCountry(countryStr);
                                l.setCountry(countryStr);
                        }
                }

                // shapes do not have address information
        }
       
        /**
         * Processing of Cities
         *
         * Fills the city list in lbl block that is required for find by name
         * It also builds up information that is required to get address info
         * for the POIs
         *
         * @param map The map.
         * @param src The map data.
         */

        private void processCities(Map map, MapDataSource src) {
                LBLFile lbl = map.getLblFile();
               
                if (!locationAutofill.isEmpty()) {
                        // collect the names of the cities
                        for (MapPoint p : src.getPoints()) {
                                if(p.isCity() && p.getName() != null)
                                        locator.addCityOrPlace(p); // Put the city info the map for missing info
                        }

                        locator.autofillCities(); // Try to fill missing information that include search of next city
                }
               
                for (MapPoint p : src.getPoints()) {
                        if (!p.isCity() || p.getName() == null)
                                continue;
                        String countryStr = p.getCountry();
                        Country thisCountry;
                        if (countryStr != null) {
                                thisCountry = lbl.createCountry(countryStr, locator.getCountryISOCode(countryStr));
                        } else {
                                thisCountry = getDefaultCountry();
                        }
                        String regionStr = p.getRegion();
                        Region thisRegion;
                        if (regionStr != null) {
                                thisRegion = lbl.createRegion(thisCountry, regionStr, null);
                        } else {
                                thisRegion = getDefaultRegion(thisCountry);
                        }
                        City thisCity;
                        if (thisRegion != null)
                                thisCity = lbl.createCity(thisRegion, p.getName(), true);
                        else
                                thisCity = lbl.createCity(thisCountry, p.getName(), true);

                        cityMap.put(p, thisCity);
                }

        }

        private void processRoads(Map map, MapDataSource src) {
                LBLFile lbl = map.getLblFile();
                MapPoint searchPoint = new MapPoint();
                for (MapLine line : src.getLines()) {
                        if(!line.isRoad())
                                continue;
                        String cityName = line.getCity();
                        String cityCountryName = line.getCountry();
                        String cityRegionName  = line.getRegion();
                        String zipStr = line.getZip();

                        if(cityName == null && locationAutofill.contains("nearest")) {
                                // Get name of next city if untagged

                                searchPoint.setLocation(line.getLocation());
                                MapPoint nextCity = locator.findNextPoint(searchPoint);

                                if(nextCity != null) {
                                        cityName = nextCity.getCity();
                                        // city/region/country fields should match to the found city
                                        cityCountryName = nextCity.getCountry();
                                        cityRegionName = nextCity.getRegion();

                                        // use the zip code only if no zip code is known
                                        if(zipStr == null)
                                                zipStr = nextCity.getZip();
                                }
                        }

                        MapRoad road = (MapRoad) line;
                        road.resetImgData();

                        City roadCity = calcCity(lbl, cityName, cityRegionName, cityCountryName);
                        if (roadCity != null)
                                road.addRoadCity(roadCity);

                        if (zipStr != null) {
                                road.addRoadZip(lbl.createZip(zipStr));
                        }

                        processRoadNumbers(road, lbl);
                }      
        }

        private void processRoadNumbers(MapRoad road, LBLFile lbl) {
                List<Numbers> numbers = road.getRoadDef().getNumbersList();
                if (numbers == null)
                        return;
                for (Numbers num : numbers) {
                        for (int i = 0; i < 2; i++) {
                                boolean leftRightFlag = (i == 0);
                                ZipCodeInfo zipInfo = num.getZipCodeInfo(leftRightFlag);
                                if (zipInfo != null && zipInfo.getZipCode() != null) {
                                        Zip zip = zipInfo.getImgZip();
                                        if (zip == null) {
                                                zip = lbl.createZip(zipInfo.getZipCode());
                                                zipInfo.setImgZip(zip);
                                        }
                                        if (zip != null)
                                                road.addRoadZip(zip);
                                }
                                CityInfo cityInfo = num.getCityInfo(leftRightFlag);
                                if (cityInfo != null) {
                                        City city = cityInfo.getImgCity();
                                        if (city == null) {
                                                city = calcCity(lbl, cityInfo.getCity(), cityInfo.getRegion(), cityInfo.getCountry());
                                                cityInfo.setImgCity(city);
                                        }
                                        if (city != null)
                                                road.addRoadCity(city);
                                }
                        }
                }
        }

        private City calcCity(LBLFile lbl, String city, String region, String country) {
                if (city == null && region == null && country == null)
                        return null;
                Country cc = (country == null) ? getDefaultCountry()
                                : lbl.createCountry(locator.normalizeCountry(country), locator.getCountryISOCode(country));
                Region cr = (region == null) ? getDefaultRegion(cc) : lbl.createRegion(cc, region, null);
                if (city == null) {
                        // if city name is unknown and region and/or country is known
                        // use empty name for the city
                        city = UNKNOWN_CITY_NAME;
                }
                if (cr != null) {
                        return lbl.createCity(cr, city, false);
                } else {
                        return lbl.createCity(cc, city, false);
                }
        }
       
        private void processPOIs(Map map, MapDataSource src) {

                LBLFile lbl = map.getLblFile();
                boolean checkedForPoiDispFlag = false;

                for (MapPoint p : src.getPoints()) {
                        // special handling for highway exits
                        if(p.isExit()) {
                                processExit(map, (MapExitPoint)p);
                        }
                        // do not process:
                        // * cities (already processed)
                        // * extended types (address information not shown in MapSource and on GPS)
                        // * all POIs except roads in case the no-poi-address option is set
                        else if (!p.isCity() && !p.hasExtendedType() &&  poiAddresses) {
                               
                                String countryStr = p.getCountry();
                                String regionStr  = p.getRegion();
                                String zipStr     = p.getZip();
                                String cityStr    = p.getCity();

                                if (locationAutofill.contains("nearest")
                                                && (countryStr == null || regionStr == null || (zipStr == null && cityStr == null))) {
                                        MapPoint nextCity = locator.findNearbyCityByName(p);

                                        if (nextCity == null)
                                                nextCity = locator.findNextPoint(p);

                                        if (nextCity != null) {
                                                if (countryStr == null)
                                                        countryStr = nextCity.getCountry();
                                                if (regionStr == null)
                                                        regionStr = nextCity.getRegion();

                                                if (zipStr == null) {
                                                        String cityZipStr = nextCity.getZip();

                                                        // Ignore list of Zips separated by ,
                                                        if (cityZipStr != null && cityZipStr.indexOf(',') < 0)
                                                                zipStr = cityZipStr;
                                                }

                                                if (cityStr == null)
                                                        cityStr = nextCity.getCity();

                                        }
                                }                              
       
                                if (countryStr != null && !checkedForPoiDispFlag) {
                                        // Different countries require different address notation

                                        poiDisplayFlags = locator.getPOIDispFlag(countryStr);
                                        checkedForPoiDispFlag = true;
                                }

                                POIRecord r = lbl.createPOI(p.getName());      
                               
                                if (cityStr != null || regionStr != null || countryStr != null) {
                                        r.setCity(calcCity(lbl, cityStr, regionStr, countryStr));
                                }

                                if (zipStr != null) {
                                        Zip zip = lbl.createZip(zipStr);
                                        r.setZip(zip);
                                }

                                if (p.getStreet() != null) {
                                        Label streetName = lbl.newLabel(p.getStreet());
                                        r.setStreetName(streetName);
                                }

                                String houseNumber = p.getHouseNumber();
                                if (houseNumber != null && !houseNumber.isEmpty() && !r.setSimpleStreetNumber(houseNumber)) {
                                        r.setComplexStreetNumber(lbl.newLabel(houseNumber));
                                }

                                String phone = p.getPhone();
                                if (phone != null && !phone.isEmpty() && !r.setSimplePhoneNumber(phone)) {
                                        r.setComplexPhoneNumber(lbl.newLabel(phone));
                                }
                       
                                poimap.put(p, r);
                        }
                }

                lbl.allPOIsDone();
        }

        private void processExit(Map map, MapExitPoint mep) {
                LBLFile lbl = map.getLblFile();
                String exitName = mep.getName();
                String ref = mep.getMotorwayRef();
                String osmId = mep.getOSMId();
                if (ref == null)
                        log.warn("Can't create exit", exitName, "(OSM id", osmId, ") doesn't have exit:road_ref tag");
                else {
                        Highway hw = highways.get(ref);
                        if (hw == null) {
                                String countryStr = mep.getCountry();
                                Country thisCountry = countryStr != null ? lbl.createCountry(locator.normalizeCountry(countryStr), locator.getCountryISOCode(countryStr)) : getDefaultCountry();
                                String regionStr = regionName != null ? regionName : mep.getRegion(); // use --region-name if set because highway will likely span regions
                                Region thisRegion = regionStr != null ? lbl.createRegion(thisCountry, regionStr, null) : getDefaultRegion(thisCountry);
                                hw = lbl.createHighway(thisRegion, ref);
                                log.info("creating highway", ref, "region:", regionStr, "country:", countryStr, "for exit:", exitName);
                                highways.put(ref, hw);
                        }
                        String exitTo = mep.getTo();
                        Exit exit = new Exit(hw);
                        String facilityDescription = mep.getFacilityDescription();
                        log.info("Creating", ref, "exit", exitName, "(OSM id", osmId +") to", exitTo, "with facility", ((facilityDescription == null)? "(none)" : facilityDescription));
                        if(facilityDescription != null) {
                                // description is TYPE,DIR,FACILITIES,LABEL
                                // (same as Polish Format)
                                String[] atts = facilityDescription.split(",");
                                int type = 0;
                                if(atts.length > 0)
                                        type = Integer.decode(atts[0]);
                                char direction = ' ';
                                if(atts.length > 1) {
                                        direction = atts[1].charAt(0);
                                        if(direction == '\'' && atts[1].length() > 1)
                                                direction = atts[1].charAt(1);
                                }
                                int facilities = 0x0;
                                if(atts.length > 2)
                                        facilities = Integer.decode(atts[2]);
                                String description = "";
                                if(atts.length > 3)
                                        description = atts[3];
                                boolean last = true;
                                // FIXME - handle multiple facilities?
                                ExitFacility ef = lbl.createExitFacility(type, direction, facilities, description, last);

                                exit.addFacility(ef);
                        }
                        mep.setExit(exit);
                        POIRecord r = lbl.createExitPOI(exitName, exit);
                        if(exitTo != null) {
                                Label ed = lbl.newLabel(exitTo);
                                exit.setDescription(ed);
                        }
                        poimap.put(mep, r);
                        // FIXME - set bottom bits of type to reflect facilities available?
                }
        }

        /**
         * Drive the map generation by stepping through the levels, generating the
         * subdivisions for the level and filling in the map elements that should
         * go into the area.
         *
         * This is fairly complex: you need to divide into subdivisions depending on
         * their size and the number of elements that will be contained.
         *
         * @param map The map.
         * @param src The data for the map.
         */

        private void makeMapAreas(Map map, LoadableMapDataSource src) {
                // The top level has to cover the whole map without subdividing, so
                // do a special check to make sure.
                LevelInfo[] levels = null;
                if (isOverviewCombined) {
                        if (mergeShapes)
                                prepShapesForMerge(src.getShapes());
                        levels = src.mapLevels();
                } else if (isOverviewComponent) {
                        levels = src.overviewMapLevels();
                } else {
                        levels = src.mapLevels();
                }
                if (levels == null) {
                        throw new ExitException("no info about levels available.");
                }
                LevelInfo levelInfo = levels[0];

                // If there is already a top level zoom, then we shouldn't add our own
                Subdivision topdiv;
                if (levelInfo.isTop()) {
                        // There is already a top level definition.  So use the values from it and
                        // then remove it from the levels definition.

                        levels = Arrays.copyOfRange(levels, 1, levels.length);

                        Zoom zoom = map.createZoom(levelInfo.getLevel(), levelInfo.getBits());
                        topdiv = makeTopArea(src, map, zoom);
                } else {
                        // We have to automatically create the definition for the top zoom level.
                        int maxBits = getMaxBits(src);
                        // If the max is larger than the top-most data level then we
                        // decrease it so that it is less.
                        if (levelInfo.getBits() <= maxBits)
                                maxBits = levelInfo.getBits() - 1;

                        // Create the empty top level
                        Zoom zoom = map.createZoom(levelInfo.getLevel() + 1, maxBits);
                        topdiv = makeTopArea(src, map, zoom);
                }

                // We start with one map data source.
                List<SourceSubdiv> srcList = Collections.singletonList(new SourceSubdiv(src, topdiv));
                if (mergeShapes && improveOverview && isOverviewComponent) {
                        recalcMultipolygons(src, levels);
                }
                src.getShapes().forEach(s -> s.setMpRel(null)); // free memory for MultipolygonRelations

                // Now the levels filled with features.
                for (LevelInfo linfo : levels) {
                        List<SourceSubdiv> nextList = new ArrayList<>();

                        Zoom zoom = map.createZoom(linfo.getLevel(), linfo.getBits());

                        for (SourceSubdiv srcDivPair : srcList) {
                                MapSplitter splitter = new MapSplitter(srcDivPair.getSource(), zoom);
                                MapArea[] areas = splitter.split(orderByDecreasingArea);
                                log.info("Map region", srcDivPair.getSource().getBounds(), "split into", areas.length, "areas at resolution", zoom.getResolution());

                                for (MapArea area : areas) {
                                        Subdivision parent = srcDivPair.getSubdiv();
                                        Subdivision div = makeSubdivision(map, parent, area, zoom);
                                        if (log.isDebugEnabled())
                                                log.debug("ADD parent-subdiv", parent, srcDivPair.getSource(), ", z=", zoom, "new=", div);
                                        nextList.add(new SourceSubdiv(area, div));
                                }
                                if (!nextList.isEmpty()) {
                                        Subdivision lastdiv = nextList.get(nextList.size() - 1).getSubdiv();
                                        lastdiv.setLast(true);
                                }
                        }
                        srcList = nextList;
                }
        }

        /**
         * for the overview map:
         * Make sure that all {@link Coord} instances are
         * identical when they are equal.
         * @param shapes the list of shapes
         */

        private static void prepShapesForMerge(List<MapShape> shapes) {
                Long2ObjectOpenHashMap<Coord> coordMap = new Long2ObjectOpenHashMap<>();
                for (MapShape s : shapes) {
                        List<Coord> points = s.getPoints();
                        int n = points.size();
                        for (int i = 0; i < n; i++) {
                                Coord co = points.get(i);
                                long key = Utils.coord2Long(co);
                                Coord repl = coordMap.get(key);
                                if (repl == null)
                                        coordMap.put(key, co);
                                else
                                        points.set(i, repl);
                        }
                }
        }

        /**
         * Create the top level subdivision.
         *
         * There must be an empty zoom level at the least detailed level. As it
         * covers the whole area in one it must be zoomed out enough so that
         * this can be done.
         *
         * Note that the width is a 16 bit quantity, but the top bit is a
         * flag and so that leaves only 15 bits into which the actual width
         * can fit.
         *
         * @param src  The source of map data.
         * @param map  The map being created.
         * @param zoom The zoom level.
         * @return The new top level subdivision.
         */

        private static Subdivision makeTopArea(MapDataSource src, Map map, Zoom zoom) {
                Subdivision topdiv = map.topLevelSubdivision(src.getBounds(), zoom);
                topdiv.setLast(true);
                return topdiv;
        }

        /**
         * Make an individual subdivision for the map.  To do this we need a link
         * to its parent and the zoom level that we are working at.
         *
         * @param map   The map to add this subdivision into.
         * @param parent The parent division.
         * @param ma     The area of the map that we are fitting into this division.
         * @param z       The zoom level.
         * @return The new subdivsion.
         */

        private Subdivision makeSubdivision(Map map, Subdivision parent, MapArea ma, Zoom z) {
                List<MapPoint> points = ma.getPoints();
                List<MapLine> lines = ma.getLines();
                List<MapShape> shapes = ma.getShapes();

                Subdivision div = map.createSubdivision(parent, ma.getFullBounds(), z);

                if (ma.hasPoints())
                        div.setHasPoints(true);
                if (ma.hasIndPoints())
                        div.setHasIndPoints(true);
                if (ma.hasLines())
                        div.setHasPolylines(true);
                if (ma.hasShapes())
                        div.setHasPolygons(true);

                div.startDivision();

                processPoints(map, div, points);

                final int res = z.getResolution();
                lines = lines.stream().filter(l -> l.getMinResolution() <= res).collect(Collectors.toList());
                shapes = shapes.stream().filter(s -> s.getMinResolution() <= res).collect(Collectors.toList());
               
                if (mergeLines) {
                        LineMergeFilter merger = new LineMergeFilter();
                        lines = merger.merge(lines, res, !hasNet, allowReverseMerge);
                }

                if (mergeShapes) {
                        ShapeMergeFilter shapeMergeFilter = new ShapeMergeFilter(res, orderByDecreasingArea);
                        shapes = shapeMergeFilter.merge(shapes);
                }

                // recalculate preserved status for all points in lines and shapes
                shapes.forEach(e -> e.getPoints().forEach(p -> p.preserved(false)));
                if (z.getLevel() == 0 && hasNet) {
                        lines.forEach(e -> e.getPoints().forEach(p -> p.preserved(p.isNumberNode()))); 
                } else {
                        lines.forEach(e -> e.getPoints().forEach(p -> p.preserved(false)));
                }
                preserveFirstLast(lines);
                if (res < 24) {
                        preserveHorizontalAndVerticalLines(res, shapes);
                }
               
                processLines(map, div, lines);
                processShapes(map, div, shapes);

                div.endDivision();

                return div;
        }

        /**
         * Mark first and last point of each line as preserved
         * @param the lines
         */

        private static void preserveFirstLast(List<MapLine> lines) {
                for (MapLine l : lines) {
                        l.getPoints().get(0).preserved(true);
                        l.getPoints().get(l.getPoints().size()-1).preserved(true);
                }
        }


        /**
         * Create the overview sections.
         *
         * @param map The map details.
         * @param src The map data source.
         */

        protected void processOverviews(Map map, MapDataSource src) {
                List<Overview> features = src.getOverviews();
                for (Overview ov : features) {
                        switch (ov.getKind()) {
                        case Overview.POINT_KIND:
                                map.addPointOverview((PointOverview) ov);
                                break;
                        case Overview.LINE_KIND:
                                map.addPolylineOverview((PolylineOverview) ov);
                                break;
                        case Overview.SHAPE_KIND:
                                map.addPolygonOverview((PolygonOverview) ov);
                                break;
                        default:
                                break;
                        }
                }
        }

        /**
         * Set all the information that appears in the header.
         */

        private void getMapInfo() {
                if (licenseFileName != null) {
                        List<String> licenseArray = new ArrayList<>();
                        try {
                                File file = new File(licenseFileName);
                                licenseArray = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
                        }
                        catch (Exception e) {
                                throw new ExitException("Error reading license file " + licenseFileName, e);
                        }
                        if ((!licenseArray.isEmpty()) && licenseArray.get(0).startsWith("\ufeff"))
                                licenseArray.set(0, licenseArray.get(0).substring(1));
                        UnaryOperator<String> replaceVariables = s -> s.replace("$MKGMAP_VERSION$", Version.VERSION)
                                        .replace("$JAVA_VERSION$", System.getProperty("java.version"))
                                        .replace("$YEAR$", Integer.toString(now.getYear()))
                                        .replace("$LONGDATE$", now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)))
                                        .replace("$SHORTDATE$", now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)))
                                        .replace("$TIME$", now.toLocalTime().toString().substring(0, 5));
                        licenseArray.replaceAll(replaceVariables);
                        mapInfo.addAll(licenseArray);
                } else {
                        mapInfo.add("Map data (c) OpenStreetMap and its contributors");
                        mapInfo.add("http://www.openstreetmap.org/copyright");
                        mapInfo.add("");
                        mapInfo.add("This map data is made available under the Open Database License:");
                        mapInfo.add("http://opendatacommons.org/licenses/odbl/1.0/");
                        mapInfo.add("Any rights in individual contents of the database are licensed under the");
                        mapInfo.add("Database Contents License: http://opendatacommons.org/licenses/dbcl/1.0/");
                        mapInfo.add("");

                        // Pad the version number with spaces so that version
                        // strings that are different lengths do not change the size and
                        // offsets of the following sections.
                        mapInfo.add("Map created with mkgmap-r"
                                        + String.format("%-10s", Version.VERSION));

                        mapInfo.add("Program released under the GPL");
                }
        }
       
        public void setMapInfo(List<String> msgs) {
                mapInfo = msgs;
        }

        public void setCopyrights(List<String> msgs) {
                copyrights = msgs;
        }      
       
        /**
         * Set all the information that appears in the header.
         *
         * @param map The map to write to.
         * @param src The source of map information.
         */

        private void processInfo(Map map, LoadableMapDataSource src) {
                // The bounds of the map.
                map.setBounds(src.getBounds());
                if (!isOverviewCombined)
                        poiDisplayFlags |= TREHeader.POI_FLAG_DETAIL;
                       
                poiDisplayFlags |= src.getPoiDispFlag();

                if(poiDisplayFlags != 0)
                        map.addPoiDisplayFlags(poiDisplayFlags);

                // You can add anything here.
                // But there has to be something, otherwise the map does not show up.
                //
                // We use it to add copyright information that there is no room for
                // elsewhere
                StringBuilder info = new StringBuilder();
                for (String s : mapInfo) {
                        info.append(s.trim()).append('\n');
                }
                if (info.length() > 0)
                        map.addInfo(info.toString());

                if (copyrights.isEmpty()) {
                        // There has to be (at least) two copyright messages or else the map
                        // does not show up.  The second and subsequent ones will be displayed
                        // at startup, although the conditions where that happens are not known.
                        // All copyright messages are displayed in BaseCamp.
                        String[] copyrightMessages = src.copyrightMessages();
                        if (copyrightMessages.length < 2)
                                map.addCopyright("program licenced under GPL v2");

                        for (String cm : copyrightMessages)
                                map.addCopyright(cm);
                } else {
                        for (String cm : copyrights)
                                map.addCopyright(cm);
                }
        }

        /**
         * Step through the points, filter and create a map point which is then added
         * to the map.
         *
         * Note that the location and resolution of map elements is relative to the
         * subdivision that they occur in.
         *
         * @param map   The map to add points to.
         * @param div   The subdivision that the points belong to.
         * @param points The points to be added.
         */

        private void processPoints(Map map, Subdivision div, List<MapPoint> points) {
                LBLFile lbl = map.getLblFile();
                div.startPoints();
                int res = div.getResolution();

                boolean haveIndPoints = false;
                int pointIndex = 1;

                // although the non-indexed points are output first,
                // pointIndex must be initialized to the number of indexed
                // points (not 1)
                for (MapPoint point : points) {
                        if (point.isCity() && point.getMinResolution() <= res) {
                                ++pointIndex;
                                haveIndPoints = true;
                        }
                }

                for (MapPoint point : points) {

                        if (point.isCity() || point.getMinResolution() > res)
                                continue;

                        String name = point.getName();

                        Point p = div.createPoint(name);
                        p.setType(point.getType());

                        if (point.hasExtendedType()) {
                                ExtTypeAttributes eta = point.getExtTypeAttributes();
                                if (eta != null) {
                                        eta.processLabels(lbl);
                                        p.setExtTypeAttributes(eta);
                                }
                        }

                        Coord coord = point.getLocation();
                        try {
                                p.setLatitude(coord.getLatitude());
                                p.setLongitude(coord.getLongitude());
                        } catch (AssertionError ae) {
                                log.error("Problem with point of type 0x" + Integer.toHexString(point.getType()) + " at " + coord.toOSMURL());
                                log.error("  Subdivision shift is " + div.getShift() +
                                                  " and its centre is at " + div.getCenter().toOSMURL());
                                log.error("  " + ae.getMessage());
                                continue;
                        }

                        POIRecord r = poimap.get(point);
                        if (r != null)
                                p.setPOIRecord(r);

                        map.addMapObject(p);
                        if (!point.hasExtendedType()) {
                                if (name != null && div.getZoom().getLevel() == 0) {
                                        if (pointIndex > 255) {
                                                log.error("Too many POIs near location", div.getCenter().toOSMURL(), "-", name,
                                                                "will be ignored");
                                        } else if (point.isExit()) {
                                                Exit e = ((MapExitPoint) point).getExit();
                                                if (e != null)
                                                        e.getHighway().addExitPoint(name, pointIndex, div);
                                        } else if (makePOIIndex) {
                                                lbl.createPOIIndex(name, pointIndex, div, point.getType());
                                        }
                                }

                                ++pointIndex;
                        }
                }

                if (haveIndPoints) {
                        div.startIndPoints();

                        pointIndex = 1; // reset to 1
                        for (MapPoint point : points) {

                                if (!point.isCity() || point.getMinResolution() > res)
                                        continue;

                                String name = point.getName();

                                Point p = div.createPoint(name);
                                int fullType = point.getType();
                                assert (fullType & 0xff) == 0 : "indPoint " + GType.formatType(fullType) + " has subtype";
                                p.setType(fullType);

                                Coord coord = point.getLocation();
                                try {
                                        p.setLatitude(coord.getLatitude());
                                        p.setLongitude(coord.getLongitude());
                                } catch (AssertionError ae) {
                                        log.error("Problem with point of type 0x" + Integer.toHexString(point.getType()) + " at " + coord.toOSMURL());
                                        log.error("  Subdivision shift is " + div.getShift() +
                                                          " and its centre is at " + div.getCenter().toOSMURL());
                                        log.error("  " + ae.getMessage());
                                        continue;
                                }

                                map.addMapObject(p);
                                if(name != null && div.getZoom().getLevel() == 0) {
                                        // retrieve the City created earlier for this
                                        // point and store the point info in it
                                        City c = cityMap.get(point);

                                        if(pointIndex > 255) {
                                                log.error("Can't set city point index for", name, "(too many indexed points in division)\n");
                                        } else {
                                                c.setPointIndex(pointIndex);
                                                c.setSubdivision(div);
                                        }
                                }

                                ++pointIndex;
                        }
                }
        }

        /**
         * Step through the lines, filter, simplify if necessary, and create a map
         * line which is then added to the map.
         *
         * Note that the location and resolution of map elements is relative to the
         * subdivision that they occur in.
         *
         * @param map   The map to add points to.
         * @param div   The subdivision that the lines belong to.
         * @param lines The lines to be added.
         */

        private void processLines(Map map, Subdivision div, List<MapLine> lines) {
                div.startLines();  // Signal that we are beginning to draw the lines.

                int res = div.getResolution();

                FilterConfig config = new FilterConfig();
                config.setResolution(res);
                config.setLevel(div.getZoom().getLevel());
                config.setHasNet(hasNet);

                LayerFilterChain normalFilters = new LayerFilterChain(config);
                LayerFilterChain keepParallelFilters = new LayerFilterChain(config);
                DouglasPeuckerFilter dp = null;
                if (enableLineCleanFilters && (res < 24)) {
                        MapFilter rounder = new RoundCoordsFilter();
                        MapFilter sizeFilter = new SizeFilter(MIN_SIZE_LINE);
                        normalFilters.addFilter(rounder);
                        normalFilters.addFilter(sizeFilter);
                        double errorForRes = dpFilterLineResMap.ceilingEntry(res).getValue();
                        if(errorForRes > 0) {
                                dp = new DouglasPeuckerFilter(errorForRes);
                                keepParallelFilters.addFilter(dp);
                        }
                        keepParallelFilters.addFilter(rounder);
                        keepParallelFilters.addFilter(sizeFilter);
                }
                RemoveObsoletePointsFilter removeObsolete = new RemoveObsoletePointsFilter();
                normalFilters.addFilter(removeObsolete);
                keepParallelFilters.addFilter(removeObsolete);
                if (dp != null)
                        normalFilters.addFilter(dp);
                for (MapFilter filter : Arrays.asList(
                                new RemoveEmpty(),
                                new LineSplitterFilter(),
                                new LinePreparerFilter(div),
                                new LineAddFilter(div, map))) {
                        normalFilters.addFilter(filter);
                        keepParallelFilters.addFilter(filter);
                }
               
                for (MapLine line : lines) {
                        if (line.getMinResolution() <= res) {
                                if (GType.isContourLine(line) || isOverviewComponent)
                                        keepParallelFilters.startFilter(line);
                                else
                                        normalFilters.startFilter(line);
                        }
                }
        }

        /**
         * Step through the polygons, filter, simplify if necessary, and create a map
         * shape which is then added to the map.
         *
         * Note that the location and resolution of map elements is relative to the
         * subdivision that they occur in.
         *
         * @param map   The map to add polygons to.
         * @param div   The subdivision that the polygons belong to.
         * @param shapes The polygons to be added.
         */

        private void processShapes(Map map, Subdivision div, List<MapShape> shapes) {
                div.startShapes();  // Signal that we are beginning to draw the shapes.

                int res = div.getResolution();

                FilterConfig config = new FilterConfig();
                config.setResolution(res);
                config.setLevel(div.getZoom().getLevel());
                config.setHasNet(hasNet);
               
                if (orderByDecreasingArea && shapes.size() > 1) {
                        // sort so that the shape with the largest area is processed first
                        shapes.sort((s1,s2) -> Long.compare(Math.abs(s2.getFullArea()), Math.abs(s1.getFullArea())));
                }

               
                LayerFilterChain filters = new LayerFilterChain(config);
                filters.addFilter(new PolygonSplitterFilter());
                if (enableLineCleanFilters && (res < 24)) {
                        filters.addFilter(new RoundCoordsFilter());
                }
                filters.addFilter(new RemoveObsoletePointsFilter());
                if (enableLineCleanFilters && (res < 24)) {
                        int sizefilterVal =  getMinSizePolygonForResolution(res);
                        if (sizefilterVal > 0)
                                filters.addFilter(new SizeFilter(sizefilterVal));
                        //DouglasPeucker behaves at the moment not really optimal at low zooms, but acceptable.
                        //Is there a similar algorithm for polygons?
                        double errorForRes = dpFilterShapeResMap.ceilingEntry(res).getValue();
                        if(errorForRes > 0)
                                filters.addFilter(new DouglasPeuckerFilter(errorForRes));
                }
                filters.addFilter(new RemoveEmpty());
                filters.addFilter(new LinePreparerFilter(div));
                filters.addFilter(new ShapeAddFilter(div, map));

                for (MapShape shape : shapes) {
                        if (shape.getMinResolution() <= res) {
                                filters.startFilter(shape);
                        }
                }
        }

        /**
         * Preserve shape points which a) lie on the shape boundary or
         * b) which appear multiple times in the shape (excluding the start
         * point which should always be identical to the end point).
         * The preserved points are kept treated specially in the
         * Line-Simplification-Filters, this should avoid artifacts like
         * white triangles in the sea for lower resolutions.    
         * @param res the current resolution
         * @param shapes list of shapes
         */

        private static void preserveHorizontalAndVerticalLines(int res, List<MapShape> shapes) {
                if (res == 24)
                        return;
                for (MapShape shape : shapes) {
                        if (shape.getMinResolution() > res)
                                continue;
                        List<Coord> points = shape.getPoints();
                        int n = points.size();
                        IdentityHashMap<Coord, Coord> coords = new IdentityHashMap<>(n);
                        Coord prev = points.get(0);
                        Coord last;
                        for (int i = 1; i < n; ++i) {
                                last = points.get(i);
                                // preserve coord instances which are used more than once,
                                // these are typically produced by the ShapeMergerFilter
                                // to connect holes
                                if (coords.get(last) == null) {
                                        coords.put(last, last);
                                } else if (!last.preserved()) {
                                        last.preserved(true);
                                }

                                // preserve the end points of horizontal and vertical lines
                                // they are very likely produced by cutting
                                if(last.getHighPrecLat() == prev.getHighPrecLat() || last.getHighPrecLon() == prev.getHighPrecLon()) {
                                        last.preserved(true);
                                        prev.preserved(true);
                                }
                                prev = last;
                        }
                }
        }

        /**
         * It is not possible to represent large maps at the 24 bit resolution.  This
         * gets the largest resolution that can still cover the whole area of the
         * map.  It is used for the top most layer.
         *
         * @param src The map data.
         * @return The largest number of bits where we can still represent the
         *         whole map.
         */

        private static int getMaxBits(MapDataSource src) {
                int topshift = Integer.numberOfLeadingZeros(src.getBounds().getMaxDimension());
                int minShift = Math.max(CLEAR_TOP_BITS - topshift, 0);
                return 24 - minShift;
        }

        public void setEnableLineCleanFilters(boolean enable) {
                this.enableLineCleanFilters = enable;
        }

        /**
         * Determine the minimum size for a polygon for the given level.
         * @param res the resolution
         * @return the size filter value
         */

        private int getMinSizePolygonForResolution(int res) {
                if (polygonSizeLimitsOpt == null)
                        return minSizePolygon;
       
                if (polygonSizeLimits == null) {
                        polygonSizeLimits = new TreeMap<>();
                        String[] desc = polygonSizeLimitsOpt.split("[, \\t\\n]+");

                        for (String s : desc) {
                                String[] keyVal = s.split("[=:]");
                                if (keyVal == null || keyVal.length != 2) {
                                        throw new ExitException("incorrect polygon-size-limits specification " + polygonSizeLimitsOpt);
                                }
       
                                try {
                                        int key = Integer.parseInt(keyVal[0]);
                                        int value = Integer.parseInt(keyVal[1]);
                                        Integer testDup = polygonSizeLimits.put(key, value);
                                        if (testDup != null) {
                                                throw new ExitException("duplicate resolution value in polygon-size-limits specification "
                                                                + polygonSizeLimitsOpt);
                                        }
                                } catch (NumberFormatException e) {
                                        throw new ExitException("polygon-size-limits specification not all numbers: " + s);
                                }
                        }
                        if (polygonSizeLimits.get(24) == null)
                                polygonSizeLimits.put(24, 0);
                }
                // return the value for the desired resolution or the next higher one
                return polygonSizeLimits.ceilingEntry(res).getValue();
        }

        /**
         * Parse an option with pairs of resolution and double values.
         *
         * @param props        the properties
         * @param optionName   the option name
         * @param defaultValue the default value for all resolutions if the option is
         *                     not given
         * @return the map
         */

        private TreeMap<Integer, Double> parseLevelOption(EnhancedProperties props, String optionName,
                        double defaultValue) {
                String option = props.getProperty(optionName);
                TreeMap<Integer, Double> levelMap = new TreeMap<>();
                if (option != null) {
                        String[] desc = option.split("[, \\t\\n]+");

                        for (String s : desc) {
                                String[] keyVal = s.split("[=:]");
                                if (keyVal == null || keyVal.length != 2) {
                                        throw new ExitException("incorrect " + optionName + " specification " + option + " at " + s);
                                }

                                try {
                                        int key = Integer.parseInt(keyVal[0]);
                                        double value = Double.parseDouble(keyVal[1]);
                                        Double testDup = levelMap.put(key, value);
                                        if (testDup != null) {
                                                throw new ExitException(
                                                                "duplicate resolution value in " + optionName + " specification " + optionName);
                                        }
                                } catch (NumberFormatException e) {
                                        throw new ExitException(optionName + " specification not all numbers: " + s);
                                }
                        }
                }
                if (levelMap.get(24) == null)
                        levelMap.put(24, defaultValue);
                return levelMap;
        }

        private static class SourceSubdiv {
                private final MapDataSource source;
                private final Subdivision subdiv;

                SourceSubdiv(MapDataSource ds, Subdivision subdiv) {
                        this.source = ds;
                        this.subdiv = subdiv;
                }

                public MapDataSource getSource() {
                        return source;
                }

                public Subdivision getSubdiv() {
                        return subdiv;
                }
        }

        private static class LineAddFilter extends BaseFilter implements MapFilter {
                private final Subdivision div;
                private final Map map;

                LineAddFilter(Subdivision div, Map map) {
                        this.div = div;
                        this.map = map;
                }

                @Override
                public void doFilter(MapElement element, MapFilterChain next) {
                        MapLine line = (MapLine) element;
                        assert line.getPoints().size() < 255 : "too many points";

                        Polyline pl = div.createLine(line.getLabels());
                        if (element.hasExtendedType()) {
                                ExtTypeAttributes eta = element.getExtTypeAttributes();
                                if (eta != null) {
                                        eta.processLabels(map.getLblFile());
                                        pl.setExtTypeAttributes(eta);
                                }
                        } else {
                                div.setPolylineNumber(pl);
                        }

                        pl.setDirection(line.isDirection());

                        pl.addCoords(line.getPoints());

                        pl.setType(line.getType());
                        if (map.getNetFile() != null && line instanceof MapRoad) {
                                if (log.isDebugEnabled())
                                        log.debug("adding road def: " + line.getName());
                                MapRoad road = (MapRoad) line;
                                RoadDef roaddef = road.getRoadDef();

                                pl.setRoadDef(roaddef);
                                if (road.hasSegmentsFollowing())
                                        pl.setLastSegment(false);

                                roaddef.addPolylineRef(pl);
                        }
                        map.addMapObject(pl);
                }
        }
       
        private static class ShapeAddFilter extends BaseFilter implements MapFilter {
                private final Subdivision div;
                private final Map map;

                ShapeAddFilter(Subdivision div, Map map) {
                        this.div = div;
                        this.map = map;
                }

                @Override
                public void doFilter(MapElement element, MapFilterChain next) {
                        MapShape shape = (MapShape) element;
                        assert shape.getPoints().size() < 255 : "too many points";

                        Polygon pg = div.createPolygon(shape.getName());

                        pg.addCoords(shape.getPoints());

                        pg.setType(shape.getType());
                        if (element.hasExtendedType()) {
                                ExtTypeAttributes eta = element.getExtTypeAttributes();
                                if (eta != null) {
                                        eta.processLabels(map.getLblFile());
                                        pg.setExtTypeAttributes(eta);
                                }
                        }
                        map.addMapObject(pg);
                }
        }

        /**
         * Find out shapes visible in the overview map which were created from a single multipolygon.
         * Typically this is the sea polygon. Create a simplified version for each.
         * @param src the map source
         * @param levels levels for the overview map
         */

        private void recalcMultipolygons(LoadableMapDataSource src, LevelInfo[] levels) {
                final int maxRes = levels[levels.length - 1].getBits();
                java.util.Map<MultiPolygonRelation, List<MapShape>> mpShapes = new LinkedHashMap<>();
                src.getShapes().stream().filter(s -> s.getMpRel() != null && s.getMinResolution() <= maxRes)
                                .forEach(s -> mpShapes.computeIfAbsent(s.getMpRel(), k -> new ArrayList<>()).add(s));
                if (mpShapes.isEmpty())
                        return;
                MapShapeComparator comparator = new MapShapeComparator(orderByDecreasingArea);
                for (Entry<MultiPolygonRelation, List<MapShape>> e : mpShapes.entrySet()) {
                        if (e.getKey().isNoRecalc())
                                continue;
                        MapShape pattern = e.getValue().get(0);
                        boolean matches = true;
                        for (MapShape s : e.getValue()) {
                                if (s.getMinResolution() != pattern.getMinResolution()
                                                || s.getMaxResolution() != pattern.getMaxResolution()
                                                || comparator.compare(s, pattern) != 0) {
                                        matches = false;
                                        break;
                                }
                        }
                        if (matches) {
                                buildMPRing(src, maxRes, pattern, e.getKey());
                                e.getValue().forEach(s -> s.setMinResolution(maxRes + 1));
                        }
                }
        }

        /**
         * Re-Render the multipolygon for the given max. resolution and create new
         * shapes with this value as maxResolution.
         *
         * @param src     the map source
         * @param res     the wanted maximum resolution
         * @param pattern pattern for the new shapes
         * @param origMp  the multipolygon relation that contains the rings at full
         *                resolution
         */

        private void buildMPRing(LoadableMapDataSource src, int res, MapShape pattern, MultiPolygonRelation origMp) {
                List<? extends Way> rings = origMp.getRings();
                Way largest = origMp.getLargestOuterRing();
                int shift = 24 - res;
                int minSize = getMinSizePolygonForResolution(res) * (1 << shift) / 2;
                GeneralRelation gr = new GeneralRelation(FakeIdGenerator.makeFakeId());
                java.util.Map<Long, Way> wayMap = new LinkedHashMap<>();

                final double dpError = dpFilterShapeResMap.ceilingEntry(res).getValue() * (1 << shift);
                for (int i = 0; i < rings.size(); i++) {
                        List<Coord> poly = new ArrayList<>(rings.get(i).getPoints());
                        boolean isLargest = largest == rings.get(i);
                        boolean tooSmall = minSize > 0 && Area.getBBox(poly).getMaxDimension() < minSize;
                        if (isLargest && tooSmall && !pattern.isSkipSizeFilter())
                                return;
                        if (tooSmall)
                                continue;
                        if (dpError > 0 && !isLargest) {
                                DouglasPeuckerFilter.douglasPeucker(poly, 0, poly.size() - 1, dpError);
                        }
                        if (poly.size() > 3) {
                                Way w = new Way(FakeIdGenerator.makeFakeId(), poly);
                                wayMap.put(w.getId(), w);
                                gr.addElement("", w);
                        }
                }
                List<List<Coord>> list = new ArrayList<>();
                if (gr.getElements().isEmpty()) {
                        return;
                } else if (gr.getElements().size() == 1) {
                        list.addAll(ShapeSplitter.clipToBounds(largest.getPoints(), src.getBounds(), null));
                } else {
                        final String codeValue = GType.formatType(pattern.getType());
                        gr.addTag("code", codeValue);
                        gr.addTag("expect-self-intersection", "true");
                        MultiPolygonRelation mp = new MultiPolygonRelation(gr, wayMap, src.getBounds());
                        mp.processElements();
                        for (Way w : wayMap.values()) {
                                if (MultiPolygonRelation.STYLE_FILTER_POLYGON.equals(w.getTag(MultiPolygonRelation.STYLE_FILTER_TAG))
                                                && codeValue.equals(w.getTag("code"))) {
                                        if (src.getBounds().contains(Area.getBBox(w.getPoints()))) {
                                                list.add(w.getPoints());
                                        } else {
                                                // we must clip to tile bounds
                                                list.addAll(ShapeSplitter.clipToBounds(w.getPoints(), src.getBounds(), null));
                                        }
                                }
                        }
                }

                for (int i = 0; i < list.size(); i++) {
                        List<Coord> poly = list.get(i);
                        MapShape newShape = pattern.copy();

                        newShape.setPoints(poly);
                        newShape.setOsmid(FakeIdGenerator.makeFakeId());
                        newShape.setMaxResolution(res);
                        newShape.setMpRel(null);
                        src.getShapes().add(newShape);
                }
        }
}