/*
 * 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;
	}
	
}
