Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2010-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.reader.osm;

import java.awt.Rectangle;
import java.awt.geom.Path2D;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.FormatException;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.filters.ShapeMergeFilter;
import uk.me.parabola.mkgmap.general.LineClipper;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.osmstyle.StyleImpl;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.Java2DConverter;

/**
 * Code to generate sea polygons from the coastline ways.
 *
 * Currently there are a number of different options.
 * Should pick one that works well and make it the default.
 *
 */

public class SeaGenerator implements OsmReadingHooks {
        private static final Logger log = Logger.getLogger(SeaGenerator.class);

        private String precompSea;
        private boolean generateSeaUsingMP = true;
        private int maxCoastlineGap;
        private boolean allowSeaSectors = true;
        private boolean extendSeaSectors;
        private String[] landTag = { "natural", "land" };
        private boolean floodblocker;
        private int fbGap = 40;
        private double fbRatio = 0.5d;
        private int fbThreshold = 20;
        private boolean fbDebug;
        private boolean checkCoastline = false;

        private ElementSaver saver;

        private List<Way> shoreline = new ArrayList<>();
        private List<Way> islands = new ArrayList<>();
        private List<Way> antiIslands = new ArrayList<>();
        private Area tileBounds;
        private boolean generateSeaBackground = true;

        private String[] coastlineFilenames;
        private StyleImpl fbRules;

        private boolean improveOverview;

        /** The size (lat and long) of the precompiled sea tiles */
        public static final int PRECOMP_RASTER = 1 << 15;
       
        // flags used in the index
        private static final byte SEA_TILE = 's';
        private static final byte LAND_TILE = 'l';
        private static final byte MIXED_TILE = 'm';
       
        private static ThreadLocal<PrecompData> precompIndex = new ThreadLocal<>();
        private static Map<String, Boolean> checkedPrecomp = new ConcurrentHashMap<>();
       
        // useful constants defining the min/max map units of the precompiled sea tiles
        private static final int MIN_LAT = Utils.toMapUnit(-90.0);
        private static final int MAX_LAT = Utils.toMapUnit(90.0);
        private static final int MIN_LON = Utils.toMapUnit(-180.0);
        private static final int MAX_LON = Utils.toMapUnit(180.0);
        private static final int INDEX_WIDTH = (getPrecompTileStart(MAX_LON) - getPrecompTileStart(MIN_LON)) / PRECOMP_RASTER;
        private static final int INDEX_HEIGHT = (getPrecompTileStart(MAX_LAT) - getPrecompTileStart(MIN_LAT)) / PRECOMP_RASTER;
       
        private static final Pattern KEY_SPLITTER = Pattern.compile(Pattern.quote("_"));
        private static final Pattern SEMICOLON_SPLITTER = Pattern.compile(Pattern.quote(";"));
        private static final short TK_NATURAL = TagDict.getInstance().xlate("natural");

        /**
         * When order-by-decreasing-area we need all bit of sea to be output consistently.
         * Unless _draworder changes things, having SEA_SIZE as BIG causes polygons beyond the
         * coastline to be shown. To hide these and have the sea show up to the high-tide
         * coastline, can set this to be very small instead (or use _draworder).
         * <p>
         * mkgmap:drawLevel can be used to override this value in the style - the default style has:
         * natural=sea { add mkgmap:skipSizeFilter=true; set mkgmap:drawLevel=2 } [0x32 resolution 10]
         * which is equivalent to Long.MAX_VALUE-2.
         */

        private static final long SEA_SIZE = Long.MAX_VALUE-2; // sea is BIG

        /**
         * Sort out options from the command line.
         * Returns true only if the option to generate the sea is active, so that
         * the whole thing is omitted if not used.
         */

        @Override
        public boolean init(ElementSaver saver, EnhancedProperties props, Style style) {
                this.saver = saver;
               
                boolean failOnIndexCheck = props.getProperty("check-precomp-sea", true);
                precompSea = props.getProperty("precomp-sea", null);
                improveOverview = props.getProperty("improve-overview", false);
                if (precompSea != null) {
                        synchronized (checkedPrecomp) {
                                initPrecompSeaIndex(precompSea, failOnIndexCheck);     
                        }
                }
                String gs = props.getProperty("generate-sea", null);
                if (gs != null) {
                        parseGenerateSeaOption(gs, precompSea != null);
                       
                        // init floodblocker and coastlinefile loader only
                        // if precompSea is not set
                        if (precompSea == null) {
                                if (floodblocker) {
                                        loadFloodblockerStyle();
                                }

                                String coastlineFileOpt = props.getProperty("coastlinefile", null);
                                if (coastlineFileOpt != null) {
                                        coastlineFilenames = coastlineFileOpt.split(",");
                                        CoastlineFileLoader.getCoastlineLoader().setCoastlineFiles(coastlineFilenames);
                                        CoastlineFileLoader.getCoastlineLoader().loadCoastlines();
                                        log.info("Coastlines loaded");
                                } else {
                                        coastlineFilenames = null;
                                }
                        }
                }

                return gs != null || precompSea != null;
        }
       
        public static void checkIndexAgainstRef(String absolutePath) {
                if (precompIndex.get() != null) {
                        precompIndex.remove();
                }

                initPrecompSeaIndex(absolutePath, true);
                if (precompIndex.get() != null) {
                        precompIndex.remove();
                }
        }
       
        private void loadFloodblockerStyle() {
                try {
                        fbRules = new StyleImpl(null, "floodblocker");
                } catch (FileNotFoundException e) {
                        log.error("Cannot load file floodblocker rules. Continue floodblocking disabled.");
                        floodblocker = false;
                }
        }

        private void parseGenerateSeaOption(String gs, boolean forPrecompSea) {
                for (String option : gs.split(",")) {
                        if ("no-mp".equals(option) || "polygon".equals(option) || "polygons".equals(option))
                                generateSeaUsingMP = false;
                        else if ("multipolygon".equals(option))
                                generateSeaUsingMP = true;
                        else if (option.startsWith("land-tag="))
                                landTag = option.substring(9).split("=");
                        else if (!forPrecompSea) {
                                // the other options are only valid if precompiled sea is not used
                                if (option.startsWith("close-gaps="))
                                        maxCoastlineGap = (int) Double.parseDouble(option.substring(11));
                                else if ("no-sea-sectors".equals(option))
                                        allowSeaSectors = false;
                                else if ("extend-sea-sectors".equals(option)) {
                                        allowSeaSectors = false;
                                        extendSeaSectors = true;
                                } else if ("floodblocker".equals(option)) {
                                        floodblocker = true;
                                } else if (option.startsWith("fbgap=")) {
                                        fbGap = (int) Double.parseDouble(option.substring("fbgap=".length()));
                                } else if (option.startsWith("fbratio=")) {
                                        fbRatio = Double.parseDouble(option.substring("fbratio=".length()));
                                } else if (option.startsWith("fbthres=")) {
                                        fbThreshold = (int) Double.parseDouble(option.substring("fbthres=".length()));
                                } else if ("fbdebug".equals(option)) {
                                        fbDebug = true;
                                } else if ("check".equals(option)) {
                                        checkCoastline = true;
                                } else {
                                        printOptionHelpMsg(forPrecompSea, option);
                                }
                        } else if (option.isEmpty()) {
                                // nothing to do
                        } else {
                                printOptionHelpMsg(forPrecompSea, option);
                        }
                }
        }

        private static void initPrecompSeaIndex(String precompSea, boolean failOnIndexCheck) {
                if (precompIndex.get() != null) {
                        return;
                }
                /**
                 * The directory of the precompiled sea tiles or <code>null</code> if
                 * precompiled sea should not be used.
                 */

                File precompSeaDir = new File(precompSea);  
                if (!precompSeaDir.exists()) {
                        log.error("Directory or zip file with precompiled sea does not exist: " + precompSea);
                        return;
                }

                String internalPath = null;    
                String indexFileName = "index.txt.gz";
                ZipFile zipFile = null;
                PrecompData precompData = null;
                try {
                        if (precompSeaDir.isDirectory()) {
                                File indexFile = new File(precompSeaDir, indexFileName);
                                if (!indexFile.exists()) {
                                        // check if the unzipped index file exists
                                        indexFileName = "index.txt";
                                        indexFile = new File(precompSeaDir, indexFileName);
                                }
                                if (indexFile.exists()) {
                                        precompData = readIndexStream(indexFileName, new FileInputStream(indexFile));
                                }
                        } else if (precompSea.endsWith(".zip")) {
                                zipFile = new ZipFile(precompSeaDir); // don't close here!
                                internalPath = "sea/";
                                ZipEntry entry = zipFile.getEntry(internalPath + indexFileName);
                                if (entry == null) {
                                        indexFileName = "index.txt";
                                        entry = zipFile.getEntry(internalPath + indexFileName);
                                }
                                if (entry == null) {
                                        internalPath = "";
                                        indexFileName = "index.txt.gz";
                                        entry = zipFile.getEntry(internalPath + indexFileName);
                                }
                                if (entry != null) {
                                        precompData = readIndexStream(indexFileName, zipFile.getInputStream(entry));
                                } else {
                                        log.error("Don't know how to read " + precompSeaDir);
                                }
                        } else {
                                log.error("Don't know how to read " + precompSeaDir);
                        }
                        if (precompData != null) {
//                              createIndexRef(precompData); // enable to create a new reference file in the current directory
                                if (!checkedPrecomp.containsKey(precompSea)) {
                                        checkedPrecomp.put(precompSea, Boolean.TRUE);
                                        try {
                                                checkIndex(precompData, failOnIndexCheck);
                                        } catch (IOException e) {
                                                Logger.defaultLogger.error("Internal error: Cannot check index file " + indexFileName + " in "
                                                                + precompSea + ". Resource sea-check.txt is probably wrong.");
                                        }
                                }
                               
                                precompData.dirFile = precompSeaDir;
                                if (zipFile != null) {
                                        precompData.precompZipFileInternalPath = internalPath;
                                        precompData.zipFile = zipFile;
                                }
                                precompIndex.set(precompData);
                        }
                } catch (IOException exp) {
                        log.error("Cannot read index file", indexFileName, "in", precompSea, exp);
                        throw new ExitException("Failed to read required index file in " + precompSeaDir);
                }
        }
       
        private static PrecompData readIndexStream(String indexFileName, InputStream indexStream) throws IOException {
                if (indexFileName.endsWith(".gz")) {
                        indexStream = new GZIPInputStream(indexStream);
                }
                PrecompData precompData = loadIndex(indexStream);
                indexStream.close();
                return precompData;
        }
       
        /**
         * Show valid generate-sea options
         * @param forPrecompSea  set to true if --precomp-sea is used
         * @param option either "help" or an option that was not recognized
         */

        private static void printOptionHelpMsg(boolean forPrecompSea, String option) {
                if(!"help".equals(option))
                        System.err.println("Unknown sea generation option '" + option + "'");
               
                System.err.println("Known sea generation options " + (forPrecompSea ? "with" : "without") + " --precomp-sea  are:");
                System.err.println("  multipolygon        use a multipolygon (default)");
                System.err.println("  polygons | no-mp    use polygons rather than a multipolygon");
                System.err.println("  land-tag=TAG=VAL    tag to use for land polygons (default natural=land)");
                if (forPrecompSea)
                        return;

                System.err.println("  no-sea-sectors      disable use of \"sea sectors\"");
                System.err.println("  extend-sea-sectors  extend coastline to reach border");
                System.err.println("  close-gaps=NUM      close gaps in coastline that are less than this distance (metres)");
                System.err.println("  floodblocker        enable the floodblocker (for multipolgon only)");
                System.err.println("  fbgap=NUM           points closer to the coastline are ignored for flood blocking (default 40)");
                System.err.println("  fbthres=NUM         min points contained in a polygon to be flood blocked (default 20)");
                System.err.println("  fbratio=NUM         min ratio (points/area size) for flood blocking (default 0.5)");
                System.err.println("  check               check for sea polygons within sea and land within land");
        }

    /**
     * Read the index from stream and populate the index grid.
     * @param fileStream already opened stream
     */

    private static PrecompData loadIndex(InputStream fileStream) throws IOException{
                PrecompData pi = null;
                LineNumberReader indexReader = new LineNumberReader(new InputStreamReader(fileStream));
                String indexLine = null;

                byte[][] indexGrid = new byte[INDEX_WIDTH + 1][INDEX_HEIGHT + 1];
                boolean detectExt = true;
                String prefix = null;
                String ext = null;

                while ((indexLine = indexReader.readLine()) != null) {
                        if (indexLine.startsWith("#")) {
                                // comment
                                continue;
                        }
                        String[] items = SEMICOLON_SPLITTER.split(indexLine);
                        if (items.length != 2) {
                                log.warn("Invalid format in index file name:", indexLine);
                                continue;
                        }
                        String precompKey = items[0];
                        byte type = updatePrecompSeaTileIndex(precompKey, items[1], indexGrid);
                        if (type == '?') {
                                log.warn("Invalid format in index file name:", indexLine);
                                continue;
                        }
                        if (type == MIXED_TILE) {
                                // make sure that all file names are using the same name scheme
                                int prePos = items[1].indexOf(items[0]);
                                if (prePos >= 0) {
                                        if (detectExt) {
                                                prefix = items[1].substring(0, prePos);
                                                ext = items[1].substring(prePos + items[0].length());
                                                detectExt = false;
                                        } else {
                                                String fname = prefix + precompKey + ext;
                                                if (!items[1].equals(fname)) {
                                                        log.warn("Unexpected file name in index file:", indexLine);
                                                }
                                        }
                                }
                        }

                }
                //
                pi = new PrecompData();
                pi.precompIndex = indexGrid;
                pi.precompSeaPrefix = prefix;
                pi.precompSeaExt = ext;
                return pi;
    }

    /**
     * Method to create the reference file that is used by checkIndex.
     * @param pi the calculated index data
     * @throws IOException in case of I/O errors
     */

//      private static void createIndexRef(PrecompData pi) throws IOException {
//              try (FileWriter fw = new FileWriter("sea-check.txt", false)) {
//                      for (int h = 1; h <= INDEX_HEIGHT; h++) {
//                              for (int w = INDEX_WIDTH; w >= 1; w--) {
//                                      fw.write(pi.precompIndex[w][h]);
//                              }
//                              fw.write('\n');
//                      }
//              }
//      }
       
        private static void checkIndex(PrecompData pi, boolean failOnIndexCheck) throws IOException {
                boolean hasError = false;
                try (BufferedInputStream is = new BufferedInputStream(SeaGenerator.class.getResourceAsStream("/sea-check.txt"))) {
                        for (int h = 1; h <= INDEX_HEIGHT; h++) {
                                for (int w = INDEX_WIDTH; w >= 1; w--) {
                                        int ref = is.read();
                                        if (ref == -1)
                                                throw new IOException("Failed to read sea-check.txt, mayby file is truncated?");

                                        String err = null;
                                        if (ref == 'l' && pi.precompIndex[w][h] == SEA_TILE) {
                                                err = "land-only tile is flooded";
                                        } else if (ref == 's' && pi.precompIndex[w][h] == LAND_TILE) {
                                                err = "sea-only tile is now land";
                                        }
                                        if (err != null) {
                                                hasError = true;
                                                int minLat = -1 * (PRECOMP_RASTER * h - MAX_LAT);
                                                int minLon = -1 * (PRECOMP_RASTER * w - MAX_LON);
                                                Coord c = new Coord(minLat + PRECOMP_RASTER / 2, minLon + PRECOMP_RASTER / 2);
                                                log.error("Precomp sea data seems to be wrong, " + err + " around " + c
                                                                + ", index key is " + minLat + "_" + minLon);
                                        }
                                }
                                is.read(); // skip new line character
                        }
                }
                if (hasError && failOnIndexCheck) {
                        throw new ExitException("Precomp sea data seems to be wrong. Use option --x-check-precomp-sea=0 to continue risking bad sea data.");
                }
        }

        /**
     * Retrieves the start value of the precompiled tile.
     * @param value the value for which the start value is calculated
     * @return the tile start value
     */

    public static int getPrecompTileStart(int value) {
        int rem = value % PRECOMP_RASTER;
        if (rem == 0) {
                return value;
        } else if (value >= 0) {
                return value - rem;
        } else {
                return value - PRECOMP_RASTER - rem;
        }
    }

        /**
         * Retrieves the end value of the precompiled tile.
         * @param value the value for which the end value is calculated
         * @return the tile end value
         */

        public static int getPrecompTileEnd(int value) {
                int rem = value % PRECOMP_RASTER;
                if (rem == 0) {
                        return value;
                } else if (value >= 0) {
                        return value + PRECOMP_RASTER - rem;
                } else {
                        return value - rem;
                }
        }      
       
        @Override
        public Set<String> getUsedTags() {
                HashSet<String> usedTags = new HashSet<>();
                if (coastlineFilenames == null) {
                        usedTags.add("natural");
                }
                if (floodblocker) {
                        usedTags.addAll(fbRules.getUsedTags());
                }

                if (log.isDebugEnabled())
                        log.debug("Sea generator used tags: " + usedTags);

                return usedTags;
        }

        /**
         * Test to see if the way is part of the shoreline and if it is
         * we save it.
         * @param way The way to test.
         */

        @Override
        public void onAddWay(Way way) {
                String natural = way.getTag(TK_NATURAL);
                if (natural == null)
                        return;
                int posn = natural.indexOf("coastline");
                if (posn < 0)
                        return;
                if (posn > 0 && natural.charAt(posn-1) != ';')
                        return;
                if (natural.length() > posn+9 && natural.charAt(posn+9) != ';')
                        return;
                if (precompSea != null)
                        splitCoastLineToLineAndShape(way, natural);
                else if (coastlineFilenames == null) {
                        // create copy of way that will become (part of) land/sea polygon
                        Way shore = new Way(way.getOriginalId(), way.getPoints());
                        shore.markAsGeneratedFrom(way);
                        shoreline.add(way);
                }
        }

        /**
         * With precompiled sea, we don't want to process all natural=coastline
         * ways as shapes without additional processing.  
         * This should avoid duplicate shapes for islands that are also in the
         * precompiled data.
         * @param way the OSM way with tag key natural
         * @param naturalVal the tag value
         */

        private void splitCoastLineToLineAndShape(Way way, String naturalVal){
                if (way.hasIdenticalEndPoints()){
                        // add a copy of this way to be able to draw it as a shape
                        Way shapeWay = new Way(way.getOriginalId(), way.getPoints());
                        shapeWay.markAsGeneratedFrom(way);
                        shapeWay.copyTags(way);
                        // change the tag so that only special rules looking for it are firing
                        shapeWay.deleteTag(TK_NATURAL);
                        shapeWay.addTag("mkgmap:removed_natural",naturalVal);
                        // tag that this way so that it is used as shape only
                        shapeWay.addTag(MultiPolygonRelation.STYLE_FILTER_TAG, MultiPolygonRelation.STYLE_FILTER_POLYGON);
                        saver.addWay(shapeWay);        
                }
                // make sure that the original (unchanged) way is not processed as a shape
                way.addTag(MultiPolygonRelation.STYLE_FILTER_TAG, MultiPolygonRelation.STYLE_FILTER_LINE);
        }
       
        /**
         * Loads the precomp sea tile with the given filename.
         * @param filename the filename of the precomp sea tile
         * @return all ways of the tile
         * @throws FileNotFoundException if the tile could not be found
         */

        private static Collection<Way> loadPrecompTile(InputStream is, String filename) {
                OsmPrecompSeaDataSource src = new OsmPrecompSeaDataSource();
                EnhancedProperties props = new EnhancedProperties();
                props.setProperty("style", "empty");
                src.config(props);
                log.info("Started loading coastlines from", filename);
                try{
                        src.parse(is, filename);
                } catch (FormatException e) {
                        log.error("Failed to read " + filename);
                        log.error(e);
                }
                log.info("Finished loading coastlines from", filename);
                return src.getElementSaver().getWays().values();
        }
       
        /**
         * Calculates the key names of the precompiled sea tiles for the bounding box.
         * The key names are compiled of {@code lat+"_"+lon}.
         * @return the key names for the bounding box
         */

        private List<String> getPrecompKeyNames() {
                Area bounds = saver.getBoundingBox();
                List<String> precompKeys = new ArrayList<>();
                for (int lat = getPrecompTileStart(bounds.getMinLat()); lat < getPrecompTileEnd(bounds
                                .getMaxLat()); lat += PRECOMP_RASTER) {
                        for (int lon = getPrecompTileStart(bounds.getMinLong()); lon < getPrecompTileEnd(bounds
                                        .getMaxLong()); lon += PRECOMP_RASTER) {
                                precompKeys.add(lat+"_"+lon);
                        }
                }
                return precompKeys;
        }
       
        /**
         * Get the tile name from the index.
         * @param precompKey The key name is compiled of {@code lat+"_"+lon}.
         * @return either "land" or "sea" or a file name or null
         */

        private static String getTileName(String precompKey){
                PrecompData pi = precompIndex.get();
                String[] tileCoords = KEY_SPLITTER.split(precompKey);
                int lat = Integer.parseInt(tileCoords[0]);
                int lon = Integer.parseInt(tileCoords[1]);
                int latIndex = (MAX_LAT-lat) / PRECOMP_RASTER;
                int lonIndex = (MAX_LON-lon) / PRECOMP_RASTER;
                byte type = pi.precompIndex[lonIndex][latIndex];
                switch (type){
                case SEA_TILE: return "sea";
                case LAND_TILE: return "land";
                case MIXED_TILE: return pi.precompSeaPrefix + precompKey + pi.precompSeaExt;
                default:  return null;
                }
        }
       

        /**
         * Update the index grid for the element identified by precompKey.
         * @param precompKey The key name is compiled of {@code lat+"_"+lon}.
         * @param fileName either "land", "sea", or a file name containing OSM data
         * @param indexGrid the previously allocated index grid  
         * @return the byte that was saved in the index grid
         */

        private static byte updatePrecompSeaTileIndex (String precompKey, String fileName, byte[][] indexGrid){
                String[] tileCoords = KEY_SPLITTER.split(precompKey);
                byte type = '?';
                if (tileCoords.length == 2){
                        int lat = Integer.parseInt(tileCoords[0]);
                        int lon = Integer.parseInt(tileCoords[1]);
                        int latIndex = (MAX_LAT - lat) / PRECOMP_RASTER;
                        int lonIndex = (MAX_LON - lon) / PRECOMP_RASTER;

                        if ("sea".equals(fileName))
                                type = SEA_TILE;
                        else if ("land".equals(fileName))
                                type = LAND_TILE;
                        else
                                type = MIXED_TILE;

                        indexGrid[lonIndex][latIndex] = type;
                }
                return type;
        }
       
        /**
         * Loads the precompiled sea tiles and adds the data to the
         * element saver.
         */

        private void addPrecompSea() {
                log.info("Load precompiled sea tiles");

                // flag if all tiles contains sea or way only
                // this is important for polygon processing
                boolean distinctTilesOnly;
               
                List<Way> landWays = new ArrayList<>();
                List<Way> seaWays = new ArrayList<>();
               
                // get the index with assignment key => sea/land/tilename
                distinctTilesOnly = loadLandAndSea(landWays, seaWays);
               
                if (generateSeaUsingMP || distinctTilesOnly) {
                        // when using multipolygons use the data directly from the precomp files
                        // also with polygons if all tiles are using either sea or land only
                        for (Way w : seaWays) {
                                w.setFullArea(SEA_SIZE);
                                saver.addWay(w);
                        }
                } else {
                        // using polygons
                        // first add the complete bounding box as sea
                        saver.addWay(createSeaWay(false));
                }
               
                // check if the land tags need to be changed
                boolean changeLandTag = landTag != null && ("natural".equals(landTag[0]) && !"land".equals(landTag[1]));
                for (Way w : landWays) {
                        if (changeLandTag) {
                                w.deleteTag(TK_NATURAL);
                                w.addTag(landTag[0], landTag[1]);
                        }
                        saver.addWay(w);
                }
        }

         
        private boolean loadLandAndSea(List<Way> landWays, List<Way> seaWays) {
                boolean distinctTilesOnly = true;
                List<java.awt.geom.Area> seaOnlyAreas = new ArrayList<>();
                List<java.awt.geom.Area> landOnlyAreas = new ArrayList<>();
               
                PrecompData pd = precompIndex.get();
                Long2ObjectOpenHashMap<Coord> commonCoordMap = new Long2ObjectOpenHashMap<>();

                for (String precompKey : getPrecompKeyNames()) {
                        String tileName = getTileName(precompKey);

                        if (tileName == null) {
                                log.error("Precompile sea tile " + precompKey + " is missing in the index. Skipping.");
                                continue;
                        }

                        if ("sea".equals(tileName) || "land".equals(tileName)) {
                                // the whole precompiled tile is filled with either land or sea
                                // => create a rectangle that covers the whole precompiled tile
                                String[] tileCoords = KEY_SPLITTER.split(precompKey);
                                int minLat = Integer.parseInt(tileCoords[0]);
                                int minLon = Integer.parseInt(tileCoords[1]);
                                Rectangle r = new Rectangle(minLon, minLat, PRECOMP_RASTER, PRECOMP_RASTER);

                                if ("sea".equals(tileName)) {
                                        seaOnlyAreas = addWithoutCreatingHoles(seaOnlyAreas, new java.awt.geom.Area(r));
                                } else {
                                        landOnlyAreas = addWithoutCreatingHoles(landOnlyAreas, new java.awt.geom.Area(r));
                                }
                        } else {
                                distinctTilesOnly = false;
                                loadMixedTile(pd, tileName, landWays, seaWays, commonCoordMap);
                        }
                }
                landWays.addAll(areaToWays(landOnlyAreas, "land", commonCoordMap));
                seaWays.addAll(areaToWays(seaOnlyAreas, "sea", commonCoordMap));

                if (improveOverview) {
                        createSeaMP(landWays, seaWays, tileBounds, commonCoordMap);
                }
                return distinctTilesOnly;
        }

        /**
         * Create a single multipolygon from all land ways(inner) and planet as
         * sea(outer) It is used to improve sea shapes at lower resolutions.
         *
         * @param landWays       the land areas
         * @param seaWays        the sea areas
         * @param tileBounds     the boundary of the tile
         * @param commonCoordMap map to produce unique Coord instances
         */

        private static void createSeaMP(List<Way> landWays, List<Way> seaWays, Area tileBounds, Long2ObjectOpenHashMap<Coord> commonCoordMap) {
                if (landWays.isEmpty() || seaWays.isEmpty())
                        return;
                log.info("improve-overview: re-creating multipolygon from", landWays.size(), "land areas");
                Map<Long, Way> wayMap = new LinkedHashMap<>();
                Way seaWay = new Way(FakeIdGenerator.makeFakeId(), uk.me.parabola.imgfmt.app.Area.PLANET.toCoords());
                wayMap.put(seaWay.getId(), seaWay);

                // join the land polygons, gives better results than simply adding the land ways
                landWays.forEach(w -> w.getPoints().forEach(Coord::resetHighwayCount));
                landWays.forEach(w -> w.getPoints().forEach(Coord::incHighwayCount));
                landWays.forEach(w -> w.getPoints().get(0).decHighwayCount());
                List<MapShape> landShapesToMerge = new ArrayList<>();
                for (int i = 0; i < landWays.size(); i++) {
                        Way w = landWays.get(i);
                        if (w.getPoints().stream().anyMatch(c -> c.getHighwayCount() > 1)) {
                                MapShape ms = new MapShape(w.getId());
                                ms.setType(1);
                                ms.setPoints(w.getPoints());
                                landShapesToMerge.add(ms);
                        } else {
                                wayMap.put(w.getId(), w);
                        }
                }
                ShapeMergeFilter mergeFilter = new ShapeMergeFilter(-1, false);
                List<MapShape> merged = mergeFilter.merge(landShapesToMerge);
                for (MapShape s : merged) {
                        s.getPoints().forEach(Coord::resetHighwayCount);
                        s.getPoints().forEach(Coord::incHighwayCount);
                        s.getPoints().get(0).decHighwayCount();
                        int n = s.getPoints().size();
                        boolean isSimple = true;
                        for (int i = 0; i < n; i++) {
                                int count = s.getPoints().get(i).getHighwayCount();
                                if (count > 2 || (count > 1 && i != 0 && i != n - 1)) {
                                        isSimple = false;
                                }
                        }
                        if (isSimple) {
                                Way w = new Way(FakeIdGenerator.makeFakeId(), s.getPoints());
                                wayMap.put(w.getId(), w);
                        } else {
                                Path2D path = Java2DConverter.createPath2D(s.getPoints());
                                path.setWindingRule(Path2D.WIND_EVEN_ODD);
                                List<List<Coord>> shapes = Java2DConverter.areaToShapes(new java.awt.geom.Area(path), commonCoordMap);
                                for (List<Coord> points : shapes) {
                                        if (Way.clockwise(points)) {
                                                Way w = new Way(FakeIdGenerator.makeFakeId(), points);
                                                wayMap.put(w.getId(), w);
                                        }
                                }
                        }
                }
                Relation gr = new GeneralRelation(FakeIdGenerator.makeFakeId());
                for (Way w : wayMap.values()) {
                        w.setClosedInOSM(true);
                        gr.addElement((w == seaWay ? "outer" : "inner"), w);
                }

                MultiPolygonRelation mpr = new MultiPolygonRelation(gr, wayMap, tileBounds) {
                        @Override
                        public Way getLargestOuterRing() {
                                if (largestOuterPolygon == null) {
                                        for (JoinedWay w : getRings()) {
                                                if (w.getOriginalWays().contains(seaWay)) {
                                                        largestOuterPolygon = w;
                                                        break;
                                                }
                                        }
                                }
                                return largestOuterPolygon;
                        }
                };
                // link all sea ways with the multipolygon, we don't call processShapes for this
                // relation!
                seaWays.forEach(w -> w.setMpRel(mpr));
        }

        private static void loadMixedTile(PrecompData pd, String tileName, List<Way> landWays, List<Way> seaWays,
                        Long2ObjectOpenHashMap<Coord> commonCoordMap) {
                try {
                        InputStream is = null;
                        if (pd.zipFile != null) {
                                ZipEntry entry = pd.zipFile.getEntry(pd.precompZipFileInternalPath + tileName);
                                if (entry != null) {
                                        is = pd.zipFile.getInputStream(entry);
                                } else {
                                        log.error("Precompiled sea tile " + tileName + " not found.");
                                }
                        } else {
                                File precompTile = new File(pd.dirFile, tileName);
                                is = new FileInputStream(precompTile);
                        }
                        if (is != null) {
                                Collection<Way> seaPrecompWays = loadPrecompTile(is, tileName);
                                if (log.isDebugEnabled())
                                        log.debug(seaPrecompWays.size(), "precomp sea ways from", tileName, "loaded.");

                                for (Way w : seaPrecompWays) {
                                        int n = w.getPoints().size();
                                        for (int i = 0; i < n; i++) {
                                                Coord p = w.getPoints().get(i);
                                                if (p.getLatitude() % PRECOMP_RASTER == 0 || p.getLongitude() % PRECOMP_RASTER == 0) {
                                                        long key = Utils.coord2Long(p);
                                                        Coord replacement = commonCoordMap.get(key);
                                                        if (replacement == null)
                                                                commonCoordMap.put(key, p);
                                                        else {
                                                                assert p.highPrecEquals(replacement);
                                                                w.getPoints().set(i, replacement);
                                                        }
                                                }
                                        }
                                       
                                        // set a new id to be sure that the precompiled ids do not
                                        // interfere with the ids of this run
                                        w.markAsGeneratedFrom(w);

                                        if ("land".equals(w.getTag(TK_NATURAL))) {
                                                landWays.add(w);
                                        } else {
                                                seaWays.add(w);
                                        }
                                }
                        }
                } catch (FileNotFoundException exp) {
                        log.error("Preompiled sea tile " + tileName + " not found.");
                } catch (Exception exp) {
                        log.error("Unexpected error reading "+ tileName, exp);
                }
        }

        /**
         * Try to merge an area with one or more other areas without creating holes.
         * If it cannot be merged, it is added to the list.
         * @param areas known areas
         * @param toAdd area to add
         * @return new list of areas
         */

        private static List<java.awt.geom.Area> addWithoutCreatingHoles(List<java.awt.geom.Area> areas,
                        final java.awt.geom.Area toAdd) {
                List<java.awt.geom.Area> result = new LinkedList<>();
                java.awt.geom.Area toMerge = new java.awt.geom.Area(toAdd);
               
                for (java.awt.geom.Area area : areas) {
                        java.awt.geom.Area mergedArea = new java.awt.geom.Area(area);
                        mergedArea.add(toMerge);
                        if (!mergedArea.isSingular()) {
                                result.add(area);
                                continue;
                        }
                        toMerge = mergedArea;
                }
                // create a sorted list with "smallest" area at the beginning
                int dimNew = Math.max(toMerge.getBounds().width, toMerge.getBounds().height);
                boolean added = false;
                for (int i = 0; i < result.size(); i++) {
                        java.awt.geom.Area area = result.get(i);
                        if (dimNew < Math.max(area.getBounds().width, area.getBounds().height)) {
                                result.add(i, toMerge);
                                added = true;
                                break;
                        }
                }
                if (!added)
                        result.add(toMerge);
                return result;
        }

        /**
         * @param area
         * @param type
         * @param commonCoordMap
         * @return
         */

        private static List<Way> areaToWays(List<java.awt.geom.Area> areas, String type,
                        Long2ObjectOpenHashMap<Coord> commonCoordMap) {
                List<Way> ways = new ArrayList<>();
                for (java.awt.geom.Area area : areas) {
                        List<List<Coord>> shapes = Java2DConverter.areaToShapes(area, commonCoordMap);
                        for (List<Coord> points : shapes) {
                                Way w = new Way(FakeIdGenerator.makeFakeId(), points);
                                w.addTag(TK_NATURAL, type);
                                ways.add(w);
                        }
                }
                return ways;
        }

        /**
         * Joins the given segments to closed ways as good as possible.
         * @param segments a list of closed and unclosed ways
         * @return a list of ways completely joined
         */

        public static List<Way> joinWays(Collection<Way> segments) {
                ArrayList<Way> joined = new ArrayList<>((int) Math.ceil(segments.size() * 0.5));
                Map<Coord, Way> beginMap = new IdentityHashMap<>();

                for (Way w : segments) {
                        if (w.hasIdenticalEndPoints()) {
                                joined.add(w);
                        } else if (w.getPoints().size() > 1){
                                beginMap.put(w.getFirstPoint(), w);
                        } else {
                                log.info("Discarding coastline", w.getBasicLogInformation(), "because it consists of less than 2 points");
                        }
                }
                segments.clear();
               
                boolean merged;
                do {
                        merged = false;
                        for (Way w1 : beginMap.values()) {
                                Way w2 = beginMap.get(w1.getLastPoint());
                                if (w2 != null) {
                                        merge(beginMap, joined, w1, w2);
                                        merged = true;
                                        break;
                                }
                        }
                } while (merged);
               
                log.info(joined.size(), "closed ways.", beginMap.size(), "unclosed ways.");
                joined.addAll(beginMap.values());
                return joined;
        }

        // merge the ways and maintain maps and list
        private static void merge(Map<Coord, Way> beginMap, List<Way> joined, Way w1, Way w2) {
                log.info("merging:", beginMap.size(), w1.getBasicLogInformation(), "with", w2.getBasicLogInformation());
                Way wm;
                if (FakeIdGenerator.isFakeId(w1.getId())) {
                        wm = w1;
                } else {
                        wm = new Way(w1.getOriginalId(), w1.getPoints());
                        wm.markAsGeneratedFrom(w1);
                        beginMap.put(wm.getFirstPoint(), wm);
                }
                beginMap.remove(w2.getFirstPoint());
                wm.getPoints().addAll(w2.getPoints().subList(1, w2.getPoints().size()));
               
                if (wm.hasIdenticalEndPoints()) {
                        joined.add(wm);
                        beginMap.remove(wm.getFirstPoint());
                }
        }

        /**
         * All done, process the saved shoreline information and construct the polygons.
         */

        @Override
        public void end() {
                tileBounds = saver.getBoundingBox();
                // precompiled sea has highest priority
                // if it is set do not perform any other algorithm
                if (precompSea != null && precompIndex.get() != null) {
                        addPrecompSea();
                        return;
                }

                if (coastlineFilenames == null) {
                        log.info("Shorelines before join", shoreline.size());
                        shoreline = joinWays(shoreline);
                } else {
                        shoreline.addAll(CoastlineFileLoader.getCoastlineLoader().getCoastlines(tileBounds));
                        log.info("Shorelines from extra file:", shoreline.size());
                }

                if (log.isInfoEnabled()) {
                        long closed = shoreline.stream().filter(Way::hasIdenticalEndPoints).count();
                        log.info("Closed shorelines", closed);
                        log.info("Unclosed shorelines", shoreline.size() - closed);
                }
               
                // clip all shoreline segments
                clipShorelineSegments();

                if(shoreline.isEmpty()) {
                        // No sea required
                        // Even though there is no sea, generate a land
                        // polygon so that the tile's background colour will
                        // match the land colour on the tiles that do contain
                        // some sea
                        // No matter if the multipolygon option is used it is
                        // only necessary to create a land polygon
                        saver.addWay(createLandWay());
                        // nothing more to do
                        return;
                }

                // handle islands (closed shoreline components) first (they're easy)
                handleIslands();
                if (maxCoastlineGap > 0) {
                        if (closeGaps()) { // there may be more islands now
                                handleIslands();
                        }
                }

                if (islands.isEmpty()) {
                        // the tile doesn't contain any islands so we can assume
                        // that it's showing a land mass that contains some, possibly
                        // enclosed, sea areas - in which case, we don't want a sea
                        // coloured background
                        generateSeaBackground = false;
                }

                // the remaining shoreline segments should intersect the boundary
                // find the intersection points and store them in a SortedMap
                NavigableMap<Double, Way> hitMap = findIntersectionPoints();
                verifyHits(hitMap);
                NavigableMap<Double, Way> copyHitMap = new TreeMap<>(hitMap);
                // generate background polygons (sea & land) that cover complete area and touch the edge
                List<Way> seaAreas = createSeaPolygons(hitMap);
                List<Way> landAreas = createLandPolygons(copyHitMap);

                Relation seaRelation = null;
                if (generateSeaUsingMP && generateSeaBackground) { // use multipolygon to cut out islands from sea
                        // generateSeaBackground is now a bit of a mismomer and really means "there are islands"
                        long multiId = FakeIdGenerator.makeFakeId();
                        log.debug("Generate seabounds relation", multiId);
                        seaRelation = new GeneralRelation(multiId);
                        seaRelation.addTag("type", "multipolygon");
                        seaRelation.addTag(TK_NATURAL, "sea");
                }

                processSeaAreas(seaAreas, seaRelation); // seaAreas meet the edge of the tile
                processLandAreas(landAreas, null); // landAreas meet the edge if the tile
                processSeaAreas(antiIslands, seaRelation); // antiIslands are seas that don't touch the edge
                processLandAreas(islands, seaRelation); // islands don't touch the edge

                if (checkCoastline) {
                        islands.addAll(landAreas);
                        antiIslands.addAll(seaAreas);
                        checkIslands(generateSeaBackground);
                }

                if (seaRelation != null) {
                        SeaPolygonRelation coastRel = saver.createSeaPolyRelation(seaRelation);
                        coastRel.setFloodBlocker(floodblocker);
                        if (floodblocker) {
                                coastRel.setFloodBlockerGap(fbGap);
                                coastRel.setFloodBlockerRatio(fbRatio);
                                coastRel.setFloodBlockerThreshold(fbThreshold);
                                coastRel.setFloodBlockerRules(fbRules.getWayRules());
                                coastRel.setLandTag(landTag[0], landTag[1]);
                                coastRel.setDebug(fbDebug);
                        }
                        saver.addRelation(coastRel);
                }

                shoreline = null;
                islands = null;
                antiIslands = null;
        }

        /**
         * These are bit of land that have been generated as polygons
         * @param areas list of land polygons
         * @param seaRelation if set, add as inner
         */

        private void processLandAreas(List<Way> areas, Relation seaRelation) {
                for (Way w : areas) {
                        w.addTag(landTag[0], landTag[1]);
                        //log.info("adding land", land);
                        if (seaRelation != null) {
                                // create a "inner" way for each island
                                seaRelation.addElement("inner", w);
                        }
                        saver.addWay(w);
                }
        }

        /**
         * These are bits of sea have been generated as polygons.
         * @param areas list of sea polygons
         * @param seaRelation if set, add as inner
         */

        private void processSeaAreas(List<Way> areas, Relation seaRelation) {
                for (Way w : areas) {
                        //log.info("adding sea", w);
                        w.setFullArea(SEA_SIZE);
                        if (seaRelation != null) {
                                seaRelation.addElement("outer", w);
                        } else {
                                w.addTag(TK_NATURAL, "sea");
                                saver.addWay(w);
                        }
                }
        }

        /**
         * Check whether land is enclosed in land or sea within sea.
         * @param seaBased true if the tile is also sea with land [multi-]polygons
         */

        private void checkIslands(boolean seaBased) {
                for (Way ai : antiIslands) {
                        Way containingLand = null;
                        Way containingSea = null;
                        for (Way i : islands) {
                                if (i.containsPointsOf(ai) && ((containingLand == null) || (containingLand.containsPointsOf(i))))
                                        containingLand = i;
                        }
                        for (Way ai2 : antiIslands) {
                                if ((ai2 != ai) && ai2.containsPointsOf(ai) && ((containingSea == null) || containingSea.containsPointsOf(ai2)))
                                        containingSea = ai2;
                        }
                        if ((containingSea != null) && (containingLand != null)) {
                                if (containingSea.containsPointsOf(containingLand))
                                        containingSea = null;
                                else if (containingLand.containsPointsOf(containingSea))
                                        containingLand = null;
                                else {
                                        log.warn("inner sea", ai, "is surrounded by both water", containingSea, "and land", containingLand);
                                        containingSea = null;
                                        containingLand = null;
                                }
                        }
                        if ((containingLand == null) && (seaBased || (containingSea != null)))
                                log.error("inner sea", ai, "is surrounded by water", containingSea == null ? "" : containingSea);
                }
               
                for (Way i : islands) {
                        Way containingLand = null;
                        Way containingSea = null;
                        for (Way ai : antiIslands) {
                                if (ai.containsPointsOf(i) && ((containingSea == null) || containingSea.containsPointsOf(ai)))
                                        containingSea = ai;
                        }
                        for (Way i2 : islands) {
                                if ((i2 != i) && i2.containsPointsOf(i) && ((containingLand == null) || (containingLand.containsPointsOf(i2))))
                                        containingLand = i2;
                        }
                        if ((containingSea != null) && (containingLand != null)) {
                                if (containingSea.containsPointsOf(containingLand))
                                        containingSea = null;
                                else if (containingLand.containsPointsOf(containingSea))
                                        containingLand = null;
                                else {
                                        log.warn("island", i, "is surrounded by both water", containingSea, "and land", containingLand);
                                        containingSea = null;
                                        containingLand = null;
                                }
                        }
                        if ((containingSea == null) && (containingLand != null))
                                log.error("island", i, "is surrounded by land", containingLand);
                }
        }

        private Way createLandWay() {
                long landId = FakeIdGenerator.makeFakeId();
                Way land = new Way(landId, tileBounds.toCoords());
                land.addTag(landTag[0], landTag[1]);
                return land;
        }

        /**
         * Create a sea polygon from the given tile bounds
         * @param enlarge if true, make sure that the polygon is slightly larger than the tile bounds
         * @return the created way
         */

        private Way createSeaWay(boolean enlarge) {
                log.info("generating sea, seaBounds=", tileBounds);
                Area bbox = tileBounds;
                long seaId = FakeIdGenerator.makeFakeId();
                if (enlarge) {
                        // the sea background area must be a little bigger than all
                        // inner land areas. this is a workaround for a multipolygon shortcoming:
                        // mp is not able to combine outer and inner if they intersect
                        // or have overlaying lines
                        // the added area will be clipped later
                        bbox = new Area(bbox.getMinLat() - 1, bbox.getMinLong() - 1, bbox.getMaxLat() + 1, bbox.getMaxLong() + 1);
                }
                Way sea = new Way(seaId, bbox.toCoords());
                sea.reverse(); // make clockwise for consistency
                sea.addTag(TK_NATURAL, "sea");
                sea.setFullArea(SEA_SIZE);
                return sea;
        }

        /**
         * Clip the shoreline ways to the bounding box of the map.
         */

        private void clipShorelineSegments() {
                List<Way> toBeRemoved = new ArrayList<>();
                List<Way> toBeAdded = new ArrayList<>();
                for (Way segment : shoreline) {
                        List<Coord> points = segment.getPoints();
                        List<List<Coord>> clipped = LineClipper.clip(tileBounds, points);
                        if (clipped != null) {
                                log.info("clipping", segment);
                                toBeRemoved.add(segment);
                                for (List<Coord> pts : clipped) {
                                        Way shore = new Way(segment.getOriginalId(), pts);
                                        shore.markAsGeneratedFrom(segment);
                                        toBeAdded.add(shore);
                                }
                        }
                }

                log.info("clipping: adding", toBeAdded.size(), ", removing", toBeRemoved.size());
                shoreline.removeAll(toBeRemoved);
                shoreline.addAll(toBeAdded);
        }

        /**
         * Pick out the closed ways and save them for later. They are removed from the
         * shore line list and added to the [anti]island list.
         */

        private void handleIslands() {
                Iterator<Way> it = shoreline.iterator();
                while (it.hasNext()) {
                        Way w = it.next();
                        if (w.hasIdenticalEndPoints()) {
                                if (Way.clockwise(w.getPoints()))
                                        antiIslands.add(w);
                                else
                                        islands.add(w);
                                it.remove();
                        }
                }
        }

        private boolean closeGaps() {
                // join up coastline segments whose end points are less than
                // maxCoastlineGap metres apart
                boolean someClosed = false, changed;
                do {
                        changed = false;
                        Iterator<Way> iter = shoreline.iterator();
                        while (!changed && iter.hasNext()) {
                                Way w1 = iter.next();
                                if (w1.hasIdenticalEndPoints())
                                        continue;
                                Coord w1e = w1.getLastPoint();
                                if (!tileBounds.onBoundary(w1e)) {
                                        Way closed = tryCloseGap(w1);
                                        if (closed != null) {
                                                saver.addWay(closed);
                                                changed = true;
                                                someClosed = true;
                                        }
                                }
                        }
                } while (changed);
                return someClosed;
        }
       
        private Way tryCloseGap(Way w1) {
                Coord w1e = w1.getLastPoint();
                Way nearest = null;
                double smallestGap = Double.MAX_VALUE;
                for (Way w2 : shoreline) {
                        if (w1 == w2 || w2.hasIdenticalEndPoints())
                                continue;
                        Coord w2s = w2.getFirstPoint();
                        if (!tileBounds.onBoundary(w2s)) {
                                double gap = w1e.distance(w2s);
                                if (gap < smallestGap) {
                                        nearest = w2;
                                        smallestGap = gap;
                                }
                        }
                }
                if (nearest != null && smallestGap < maxCoastlineGap) {
                        Coord w2s = nearest.getFirstPoint();
                        log.warn("Bridging " + (int) smallestGap + "m gap in coastline from " + w1e.toOSMURL() + " to "
                                        + w2s.toOSMURL());
                        Way wm;
                        if (FakeIdGenerator.isFakeId(w1.getId())) {
                                wm = w1;
                        } else {
                                wm = new Way(w1.getOriginalId());
                                wm.markAsGeneratedFrom(w1);
                                shoreline.remove(w1);
                                shoreline.add(wm);
                                wm.getPoints().addAll(w1.getPoints());
                                wm.copyTags(w1);
                        }
                        wm.getPoints().addAll(nearest.getPoints());
                        shoreline.remove(nearest);
                        // make a line that shows the filled gap
                        Way w = new Way(FakeIdGenerator.makeFakeId());
                        w.addTag(TK_NATURAL, "mkgmap:coastline-gap");
                        w.addPoint(w1e);
                        w.addPoint(w2s);
                        return w;
                }
                return null;
        }

        /**
         * Add lines to ways that touch or cross the sea bounds so that the way is closed along the edges of the bounds.
         * Adds complete edges or parts of them. This is done counter-clockwise.  
         * @param hitMap A map of the 'hits' where the shore line intersects the boundary.  
         */

        private List<Way> createLandPolygons(NavigableMap<Double, Way> hitMap) {
                NavigableSet<Double> hits = hitMap.navigableKeySet();
                List<Way> areas = new ArrayList<>();
                while (!hits.isEmpty()) {
                        Double hFirst = hits.first();
                        Double hStart = hFirst, hEnd;
                        Way w = new Way(hitMap.get(hFirst).getOriginalId());
                        w.markAsGeneratedFrom(hitMap.get(hFirst));
                        boolean finished = false;
                        do {
                                Way segment = hitMap.get(hStart);
                                log.info("current hit:", hStart, "adding:", segment);
                                segment.getPoints().forEach(w::addPointIfNotEqualToLastPoint);
                                hits.remove(hStart);
                                hEnd = getEdgeHit(tileBounds, segment.getLastPoint());
                                if (hEnd < hStart) // gone all the way around
                                        finished = true;
                                else { // if another, join it on
                                        hStart = hits.higher(hEnd);
                                        if (hStart == null) {
                                                hFirst += 4;
                                                finished = true;
                                        }
                                }
                                if (finished)
                                        hStart = hFirst;
                                addCorners(w, hEnd, hStart);
                        } while (!finished);
                        w.addPoint(w.getFirstPoint()); // close shape
                        log.info("adding landPoly, hits.size()", hits.size());
                        areas.add(w);
                }
                return areas;
        }

        /**
         * Add lines to ways that touch or cross the sea bounds so that the way is closed along the edges of the bounds.
         * Adds complete edges or parts of them. This is done clockwise.
         * This is much the same as createLandPolygons, but in reverse.
         * @param hitMap A map of the 'hits' where the shore line intersects the boundary.
         */

        private List<Way> createSeaPolygons(NavigableMap<Double, Way> hitMap) {
                NavigableSet<Double> hits = hitMap.navigableKeySet();
                List<Way> areas = new ArrayList<>();
                while (!hits.isEmpty()) {
                        Double hFirst = hits.last();
                        Double hStart = hFirst, hEnd;
                        Way w = new Way(hitMap.get(hFirst).getOriginalId());
                        w.markAsGeneratedFrom(hitMap.get(hFirst));
                        boolean finished = false;
                        do {
                                Way segment = hitMap.get(hStart);
                                log.info("current hit:", hStart, "adding:", segment);
                                segment.getPoints().forEach(w::addPointIfNotEqualToLastPoint);
                                hits.remove(hStart);
                                hEnd = getEdgeHit(tileBounds, segment.getLastPoint());
                                if (hEnd > hStart) // gone all the way around
                                        finished = true;
                                else { // if another, join it on
                                        hStart = hits.lower(hEnd);
                                        if (hStart == null) {
                                                hEnd += 4;
                                                finished = true;
                                        }
                                }
                                if (finished)
                                        hStart = hFirst;
                                addCorners(w, hEnd, hStart);
                        } while (!finished);
                        w.addPoint(w.getFirstPoint()); // close shape
                        log.info("adding seaPoly, hits.size()", hits.size());
                        areas.add(w);
                }
                return areas;
        }

        /**
         * Append corner points to the way if necessary, to give lines along the edges of the bounds
         * It is possible that the line needs to go all the way around the tile!
         * The relationship between hFrom and hTo determines the direction
         * @param w the way
         * @param hFrom going from this edgeHit (0 >= hit < 8)
         * @param hTo to this edgeHit (ditto)
         */

        private void addCorners(Way w, double hFrom, double hTo) {
                int startEdge = (int)hFrom;
                int endEdge = (int)hTo;
                int direction, toCorner;
                if (hFrom < hTo) { // increasing, anti-clockwise, land
                        direction = +1;
                        toCorner = 1;
                } else { // decreasing, clockwise, sea
                        direction = -1;
                        toCorner = 0; // (int)hFrom does the -1
                }
                log.debug("addCorners", hFrom, hTo, direction, startEdge, endEdge, toCorner);
                while (startEdge != endEdge) {
                        Coord p = getPoint(tileBounds, (startEdge + toCorner));
                        w.addPointIfNotEqualToLastPoint(p);
                        startEdge += direction;
                }
        }

        /**
         * Find the points where the remaining shore line segments intersect with the
         * map boundary.
         * @return A map of the 'hits' where the shore line intersects the boundary.
         */

        private NavigableMap<Double, Way> findIntersectionPoints() {
                NavigableMap<Double, Way> hitMap = new TreeMap<>();
                for (Way w : shoreline) {
                        Coord pStart = w.getFirstPoint();
                        Coord pEnd = w.getLastPoint();

                        Double hStart = getEdgeHit(tileBounds, pStart);
                        Double hEnd = getEdgeHit(tileBounds, pEnd);
                        if (hStart != null && hEnd != null) {
                                // nice case: both ends touch the boundary
                                log.debug("hits:", hStart, hEnd);
                                hitMap.put(hStart, w);
                                hitMap.put(hEnd, null); // put this for verifyHits which then deletes it
                        } else {
                                /*
                                 * This problem occurs usually when the shoreline is cut by osmosis (e.g. country-extracts from geofabrik)
                                 * and so a tile, covering land outside the selected area, has bits of unclosed shoreline that
                                 * don't start and finish outside the tile.
                                 * There are various possibilities to show a reasonable map, but there is no full solution.
                                 * mkgmap offers various options:
                                 * 1. Use --precomp-sea=... This has all the coastline and the following is N/A.
                                 * 2. Close short gaps in the coastline; eg --generate-sea=...,close-gaps=500
                                 *    Harbour mouths are often fixed by this.
                                 * 3. Create a "sea sector" for this shoreline segment. This is a right-angle triangle where the
                                 *    the hypotenuse is the shoreline. "sea sector" is a slight mis-nomer because, if the
                                 *    tile is sea-based, a "land sector" is created. Often this will show the coast in
                                 *    a meaningful way, but it can create a self-intersecting polygons and, if other bits of
                                 *    shoreline that reach the edge of the tile cause this area to be the same type, it won't show
                                 * 4. Extend the ends of the shoreline to the nearest edge of the tile with ...,extend-sea-sectors
                                 *    This, in conjunction with close-gaps, normally works well but it isn't foolproof.
                                 */

                                if (allowSeaSectors) {
                                        Way seaOrLand = new Way(w.getOriginalId(), w.getPoints());
                                        seaOrLand.markAsGeneratedFrom(w);
                                        int startLat = pStart.getHighPrecLat();
                                        int startLon = pStart.getHighPrecLon();
                                        int endLat = pEnd.getHighPrecLat();
                                        int endLon = pEnd.getHighPrecLon();
                                        boolean startLatIsCorner = (startLat > endLat) == (startLon > endLon);
                                        int cornerLat, cornerLon;
                                        if (generateSeaBackground) { // already have islands, so do this likewise
                                                startLatIsCorner = !startLatIsCorner;
                                                islands.add(seaOrLand);
                                        } else { // no islands, so more chance of sea being seen
                                                antiIslands.add(seaOrLand);
                                        }
                                        if (startLatIsCorner) {
                                                cornerLat = startLat;
                                                cornerLon = endLon;
                                        } else {
                                                cornerLat = endLat;
                                                cornerLon = startLon;
                                        }
                                        seaOrLand.addPoint(Coord.makeHighPrecCoord(cornerLat, cornerLon));
                                        seaOrLand.addPoint(pStart);
                                        log.info("seaSector:", islands.size(), antiIslands.size(), startLatIsCorner, Way.clockwise(seaOrLand.getPoints()), seaOrLand.getBasicLogInformation());
                                } else if (extendSeaSectors) {
                                        // join to nearest tile border
                                        if (null == hStart) {
                                                hStart = getNextEdgeHit(tileBounds, pStart);
                                                w.getPoints().add(0, getPoint(tileBounds, hStart));
                                        }
                                        if (null == hEnd) {
                                                hEnd = getNextEdgeHit(tileBounds, pEnd);
                                                w.getPoints().add(getPoint(tileBounds, hEnd));
                                        }
                                        log.debug("hits (second try):", hStart, hEnd);
                                        hitMap.put(hStart, w);
                                        hitMap.put(hEnd, null); // put this for verifyHits which then deletes it
                                } else {
                                        // Can't produce a polygon for the land/sea.
                                        // The original coastline might show, depending on the style
                                        log.error("Unresolved section of coastline", w.getBasicLogInformation());
                                }
                        }
                }
                return hitMap;
        }

        /*
         * Check the hitHap has alternating start & end of ways - adjacent coastlines on the tile
         * boundary must be in opposite directions. There may be other errors, for instance crossing (twice)
         * due to extendSeaSectors when there is another bit of coastline in the gap, that this doesn't detect.
         * After checking, the end hit is removed
         */

        private void verifyHits(NavigableMap<Double, Way> hitMap) {
                log.debug("Shorelines", shoreline.size(), "Islands", islands.size(), "Seas", antiIslands.size(), "hits", hitMap.size());
                NavigableSet<Double> hits = hitMap.navigableKeySet();
                Iterator<Double> iter = hits.iterator();
                int lastStatus = 0, thisStatus;
                Double lastHit = 0.0;
                while (iter.hasNext()) {
                        Double aHit = iter.next();
                        Way segment = hitMap.get(aHit);
                        log.debug("hitmap", aHit, segment);
                        if (segment == null) {
                                thisStatus = -1;
                                iter.remove();
                        } else {
                                thisStatus = +1;
                        }
                        if (thisStatus == lastStatus)
                                log.error("Adjacent coastlines hit tile edge in same direction at", getPoint(tileBounds, lastHit), "and", getPoint(tileBounds, aHit), segment);
                        lastStatus = thisStatus;
                        lastHit = aHit;
                }
        }

        // create the point where the shoreline hits the sea bounds
        private static Coord getPoint(Area a, double edgePos) {
                log.info("getPoint:", a, edgePos);
                int aMinLongHP = a.getMinLong() << Coord.DELTA_SHIFT;
                int aMaxLongHP = a.getMaxLong() << Coord.DELTA_SHIFT;
                int aMinLatHP = a.getMinLat() << Coord.DELTA_SHIFT;
                int aMaxLatHP = a.getMaxLat() << Coord.DELTA_SHIFT;
                int platHp;
                int plonHp;
                int edge = (int) edgePos;
                double t = edgePos - edge;
                if (edge >= 4)
                        edge -= 4;
                switch (edge) {
                case 0: // southern
                        platHp = aMinLatHP;
                        plonHp = (int) Math.round(aMinLongHP + t * (aMaxLongHP - aMinLongHP));
                        break;
                case 1: // eastern
                        platHp = (int) Math.round(aMinLatHP + t * (aMaxLatHP - aMinLatHP));
                        plonHp = aMaxLongHP;
                        break;
                case 2: // northern
                        platHp = aMaxLatHP;
                        plonHp = (int) Math.round(aMaxLongHP - t * (aMaxLongHP - aMinLongHP));
                        break;
                case 3: // western
                        platHp = (int) Math.round(aMaxLatHP - t * (aMaxLatHP - aMinLatHP));
                        plonHp = aMinLongHP;
                        break;
                default:
                        throw new MapFailedException("GetPoint edge: " + edgePos);
                }
                return Coord.makeHighPrecCoord(platHp, plonHp);
        }

        /**
         * Calculate a Double that represents the position where the given point touches
         * the boundary.
         * Assumes that, if the way crosses the boundary, it has been cut so the end point
         * is exactly on the boundary.
         *
         * @param a the boundary
         * @param p the point
         * @return null if the point is not touching the boundary, else a value
         *         between 0.0 (inclusive) and 4.0 (exclusive), where 0 means the lower
         *         left corner, 0.5 means the middle of the bottom edge, 1.5 the
         *         middle of the right edge, 4 would be the lower left corner again
         */

        private static Double getEdgeHit(Area a, Coord p) {
                final int toleranceHp = 0; // tolerance24 << Coord.DELTA_SHIFT;
                final int latHp = p.getHighPrecLat();
                final int lonHp = p.getHighPrecLon();
                final int minLatHp = a.getMinLat() << Coord.DELTA_SHIFT;
                final int maxLatHp = a.getMaxLat() << Coord.DELTA_SHIFT;
                final int minLongHp = a.getMinLong() << Coord.DELTA_SHIFT;
                final int maxLongHp = a.getMaxLong() << Coord.DELTA_SHIFT;

                log.info(String.format("getEdgeHit: (%d %d) (%d %d %d %d)", latHp, lonHp, minLatHp, minLongHp, maxLatHp, maxLongHp));
                if (latHp <= minLatHp + toleranceHp) {
                        return (double) (lonHp - minLongHp) / (maxLongHp - minLongHp);
                } else if (lonHp >= maxLongHp - toleranceHp) {
                        return 1 + ((double) (latHp - minLatHp) / (maxLatHp - minLatHp));
                } else if (latHp >= maxLatHp - toleranceHp) {
                        return 2 + ((double) (maxLongHp - lonHp) / (maxLongHp - minLongHp));
                } else if (lonHp <= minLongHp + toleranceHp) {
                        return 3 + ((double) (maxLatHp - latHp) / (maxLatHp - minLatHp));
                        // if exactly on bottom LHS corner (4), will have been caught by first case (0)
                }
                return null;
        }

        /**
         * Find the nearest edge for supplied Coord p.
         */

        private static Double getNextEdgeHit(Area a, Coord p) {
                final int latHp = p.getHighPrecLat();
                final int lonHp = p.getHighPrecLon();
                final int minLatHp = a.getMinLat() << Coord.DELTA_SHIFT;
                final int maxLatHp = a.getMaxLat() << Coord.DELTA_SHIFT;
                final int minLongHp = a.getMinLong() << Coord.DELTA_SHIFT;
                final int maxLongHp = a.getMaxLong() << Coord.DELTA_SHIFT;

                log.info(String.format("getNextEdgeHit: (%d %d) (%d %d %d %d)", latHp, lonHp, minLatHp, minLongHp, maxLatHp, maxLongHp));
                // shortest distance to border (init with distance to southern border)
                int min = latHp - minLatHp;
                // number of edge as used in getEdgeHit.
                // 0 = bottom
                // 1 = right
                // 2 = upper
                // 3 = western edge of Area a
                int i = 0;
                // normalized position at border (0..1)
                double t = ((double) (lonHp - minLongHp)) / (maxLongHp - minLongHp);
                // now compare distance to eastern border with already known distance
                if (maxLongHp - lonHp < min) {
                        // update data if distance is shorter
                        min = maxLongHp - lonHp;
                        i = 1;
                        t = ((double) (latHp - minLatHp)) / (maxLatHp - minLatHp);
                }
                // same for northern border
                if (maxLatHp - latHp < min) {
                        min = maxLatHp - latHp;
                        i = 2;
                        t = ((double) (maxLongHp - lonHp)) / (maxLongHp - minLongHp);
                }
                // same for western border
                if (lonHp - minLongHp < min) {
                        i = 3;
                        t = ((double) (maxLatHp - latHp)) / (maxLatHp - minLatHp);
                }
                // now created the EdgeHit for found values
                return i + t;
        }

        /**
         * Helper class for threadlocal vars
         */

        private static class PrecompData {
                /**
                 * The index is a grid [lon][lat]. Each element defines the content of one precompiled
                 * sea tile which are {@link #SEA_TYPE}, {@link #LAND_TYPE}, or {@link #MIXED_TYPE}, or 0 for unknown
                 */

                private byte[][] precompIndex;
                private String precompSeaExt;
                private String precompSeaPrefix;
                private String precompZipFileInternalPath;
                private ZipFile zipFile;
                private File dirFile;
        }
       
}