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