/*
* 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.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.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
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.general.LineClipper;
import uk.me.parabola.mkgmap.general.LoadableMapDataSource;
import uk.me.parabola.mkgmap.osmstyle.StyleImpl;
import uk.me.parabola.mkgmap.reader.osm.xml.Osm5PrecompSeaDataSource;
import uk.me.parabola.util.EnhancedProperties;
/**
* 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
extends OsmReadingHooksAdaptor
{
private static final Logger log =
Logger.
getLogger(SeaGenerator.
class);
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 ElementSaver saver
;
private List<Way
> shoreline =
new ArrayList<Way
>();
private boolean roadsReachBoundary
; // todo needs setting somehow
private boolean generateSeaBackground =
true;
private String[] coastlineFilenames
;
private StyleImpl fbRules
;
/** The size (lat and long) of the precompiled sea tiles */
public final static int PRECOMP_RASTER =
1 << 15;
/**
* The directory of the precompiled sea tiles or <code>null</code> if
* precompiled sea should not be used.
*/
private File precompSeaDir
;
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<PrecompData
>();
// 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 final static Pattern keySplitter =
Pattern.
compile(Pattern.
quote("_"));
private static final List<Class<? extends LoadableMapDataSource
>> precompSeaLoader
;
static {
String[] sources =
{
"uk.me.parabola.mkgmap.reader.osm.bin.OsmBinPrecompSeaDataSource",
// must be last as it is the default
"uk.me.parabola.mkgmap.reader.osm.xml.Osm5PrecompSeaDataSource",
};
precompSeaLoader =
new ArrayList<Class<? extends LoadableMapDataSource
>>();
for (String source : sources
) {
try {
@
SuppressWarnings({ "unchecked" })
Class<? extends LoadableMapDataSource
> c =
(Class<? extends LoadableMapDataSource
>) Class
.
forName(source
);
precompSeaLoader.
add(c
);
} catch (ClassNotFoundException e
) {
// not available, try the rest
} catch (NoClassDefFoundError e
) {
// not available, try the rest
}
}
}
/**
* 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.
*/
public boolean init
(ElementSaver saver, EnhancedProperties props
) {
this.
saver = saver
;
String precompSea = props.
getProperty("precomp-sea",
null);
if (precompSea
!=
null) {
precompSeaDir =
new File(precompSea
);
if (precompSeaDir.
exists()){
if (precompIndex.
get() ==
null) {
PrecompData precompData =
null;
String internalPath =
null;
InputStream indexStream =
null;
String indexFileName =
"index.txt.gz";
ZipFile zipFile =
null;
try{
if (precompSeaDir.
isDirectory()){
File indexFile =
new File(precompSeaDir, indexFileName
);
if (indexFile.
exists() ==
false) {
// check if the unzipped index file exists
indexFileName =
"index.txt";
indexFile =
new File(precompSeaDir, indexFileName
);
}
if (indexFile.
exists()) {
indexStream =
new FileInputStream(indexFile
);
}
} else if (precompSea.
endsWith(".zip")){
zipFile =
new ZipFile(precompSeaDir
);
internalPath =
"sea";
ZipEntry entry = zipFile.
getEntry(internalPath
);
if (entry ==
null)
internalPath =
"";
else
internalPath = internalPath +
"/";
entry = zipFile.
getEntry(internalPath + indexFileName
);
if (entry ==
null){
indexFileName =
"index.txt";
entry = zipFile.
getEntry(internalPath + indexFileName
);
}
if (entry
!=
null){
indexStream = zipFile.
getInputStream(entry
);
}
} else {
log.
error("Don't know how to read " + precompSeaDir
);
}
if (indexStream
!=
null){
if (indexFileName.
endsWith(".gz")) {
indexStream =
new GZIPInputStream(indexStream
);
}
try{
precompData = loadIndex
(indexStream
);
} catch (IOException exp
) {
log.
error("Cannot read index file " + indexFileName,
exp
);
}
if (precompData
!=
null){
if (zipFile
!=
null){
precompData.
precompZipFileInternalPath = internalPath
;
precompData.
zipFile = zipFile
;
}
precompIndex.
set(precompData
);
}
indexStream.
close();
}
} catch (IOException exp
) {
log.
error("Cannot read index file " + indexFileName,
exp
);
}
precompIndex.
set(precompData
);
}
} else {
log.
error("Directory or zip file with precompiled sea does not exist: "
+ precompSea
);
System.
err.
println("Directory or zip file with precompiled sea does not exist: "
+ precompSea
);
precompSeaDir =
null;
}
}
String gs = props.
getProperty("generate-sea",
null);
boolean generateSea = gs
!=
null || precompSea
!=
null;
if (gs
!=
null) {
for(String o : gs.
split(",")) {
if("no-mp".
equals(o
) ||
"polygon".
equals(o
) ||
"polygons".
equals(o
))
generateSeaUsingMP =
false;
else if("multipolygon".
equals(o
))
generateSeaUsingMP =
true;
else if(o.
startsWith("land-tag="))
landTag = o.
substring(9).
split("=");
else if (precompSea ==
null) {
// the other options are valid only if not using precompiled sea data
if(o.
startsWith("close-gaps="))
maxCoastlineGap =
(int)Double.
parseDouble(o.
substring(11));
else if("no-sea-sectors".
equals(o
))
allowSeaSectors =
false;
else if("extend-sea-sectors".
equals(o
)) {
allowSeaSectors =
false;
extendSeaSectors =
true;
}
else if("floodblocker".
equals(o
))
floodblocker =
true;
else if(o.
startsWith("fbgap="))
fbGap =
(int)Double.
parseDouble(o.
substring("fbgap=".
length()));
else if(o.
startsWith("fbratio="))
fbRatio =
Double.
parseDouble(o.
substring("fbratio=".
length()));
else if(o.
startsWith("fbthres="))
fbThreshold =
(int)Double.
parseDouble(o.
substring("fbthres=".
length()));
else if("fbdebug".
equals(o
))
fbDebug =
true;
}
else if(o.
isEmpty())
continue;
else {
if(!"help".
equals(o
))
System.
err.
println("Unknown sea generation option '" + o +
"'");
System.
err.
println("Known sea generation options are:");
System.
err.
println(" multipolygon use a multipolygon (default)");
System.
err.
println(" polygons | no-mp use polygons rather than a multipolygon");
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(" land-tag=TAG=VAL tag to use for land polygons (default natural=land)");
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)");
}
}
// init floodblocker and coastlinefile loader only
// if precompSea is not set
if (precompSea ==
null) {
if (floodblocker
) {
try {
fbRules =
new StyleImpl
(null,
"floodblocker");
} catch (FileNotFoundException e
) {
log.
error("Cannot load file floodblocker rules. Continue floodblocking disabled.");
floodblocker =
false;
}
}
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 generateSea
;
}
/**
* Read the index from stream and populate the index grid.
* @param fileStream already opened stream
*/
private PrecompData loadIndex
(InputStream fileStream
) throws IOException{
int indexWidth =
(SeaGenerator.
getPrecompTileStart(MAX_LON
) - SeaGenerator.
getPrecompTileStart(MIN_LON
)) / SeaGenerator.
PRECOMP_RASTER;
int indexHeight =
(SeaGenerator.
getPrecompTileStart(MAX_LAT
) - SeaGenerator.
getPrecompTileStart(MIN_LAT
)) / SeaGenerator.
PRECOMP_RASTER;
PrecompData pi =
null;
LineNumberReader indexReader =
new LineNumberReader(
new InputStreamReader(fileStream
));
Pattern csvSplitter =
Pattern.
compile(Pattern
.
quote(";"));
String indexLine =
null;
byte[][] indexGrid =
new byte[indexWidth+
1][indexHeight+
1];
boolean detectExt =
true;
String prefix =
null;
String ext =
null;
while ((indexLine = indexReader.
readLine()) !=
null) {
if (indexLine.
startsWith("#")) {
// comment
continue;
}
String[] items = csvSplitter.
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 {
StringBuilder sb =
new StringBuilder(prefix
);
sb.
append(precompKey
);
sb.
append(ext
);
if (items
[1].
equals(sb.
toString()) ==
false){
log.
warn("Unexpected file name in index file:",
indexLine
);
}
}
}
}
}
//
pi =
new PrecompData
();
pi.
precompIndex = indexGrid
;
pi.
precompSeaPrefix = prefix
;
pi.
precompSeaExt = ext
;
return pi
;
}
/**
* 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
;
}
}
public Set<String> getUsedTags
() {
HashSet<String> usedTags =
new HashSet<String>();
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.
*/
public void onAddWay
(Way way
) {
String natural = way.
getTag("natural");
if(natural
!=
null) {
if("coastline".
equals(natural
)) {
way.
deleteTag("natural");
if (coastlineFilenames ==
null && precompSeaDir ==
null)
shoreline.
add(way
);
if (precompSeaDir
!=
null) {
// add a copy of this way to be able to draw the coastline which is not possible with precompiled sea
Way coastlineWay =
new Way
(FakeIdGenerator.
makeFakeId(), way.
getPoints());
coastlineWay.
addTag("natural",
"coastline");
// tag that this way is used as line only
coastlineWay.
addTag(MultiPolygonRelation.
STYLE_FILTER_TAG, MultiPolygonRelation.
STYLE_FILTER_LINE);
saver.
addWay(coastlineWay
);
}
} else if (natural.
contains(";")) {
// cope with compound tag value
String others =
null;
boolean foundCoastline =
false;
for(String n : natural.
split(";")) {
if("coastline".
equals(n.
trim()))
foundCoastline =
true;
else if(others ==
null)
others = n
;
else
others +=
";" + n
;
}
if(foundCoastline
) {
way.
deleteTag("natural");
if(others
!=
null)
way.
addTag("natural", others
);
if (coastlineFilenames ==
null && precompSeaDir ==
null)
shoreline.
add(way
);
if (precompSeaDir
!=
null) {
// add a copy of this way to be able to draw the coastline which is not possible with precompiled sea
Way coastlineWay =
new Way
(FakeIdGenerator.
makeFakeId(), way.
getPoints());
coastlineWay.
addTag("natural",
"coastline");
// tag that this way is used as line only
coastlineWay.
addTag(MultiPolygonRelation.
STYLE_FILTER_TAG, MultiPolygonRelation.
STYLE_FILTER_LINE);
saver.
addWay(coastlineWay
);
}
}
}
}
}
/**
* Creates a reader for the given filename of the precomiled sea tile.
* @param filename precompiled sea tile
* @return the reader for the tile
*/
private static OsmMapDataSource createTileReader
(String filename
) {
for (Class<? extends LoadableMapDataSource
> loader : precompSeaLoader
) {
try {
LoadableMapDataSource src = loader.
newInstance();
if (filename
!=
null && src
instanceof OsmMapDataSource
&& src.
isFileSupported(filename
))
return (OsmMapDataSource
) src
;
} catch (InstantiationException e
) {
// try the next one.
} catch (IllegalAccessException e
) {
// try the next one.
} catch (NoClassDefFoundError e
) {
// try the next one
}
}
// Give up and assume it is in the XML format. If it isn't we will get
// an error soon enough anyway.
return new Osm5PrecompSeaDataSource
();
}
/**
* 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 Collection<Way
> loadPrecompTile
(InputStream is,
String filename
) {
OsmMapDataSource src = createTileReader
(filename
);
src.
config(new EnhancedProperties
());
log.
info("Started loading coastlines from", filename
);
try{
src.
load(is
);
} 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<String>();
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 String getTileName
(String precompKey
){
PrecompData pi = precompIndex.
get();
String[] tileCoords = keySplitter.
split(precompKey
);
int lat =
Integer.
valueOf(tileCoords
[0]);
int lon =
Integer.
valueOf(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 byte updatePrecompSeaTileIndex
(String precompKey,
String fileName,
byte[][] indexGrid
){
String[] tileCoords = keySplitter.
split(precompKey
);
byte type =
'?';
if (tileCoords.
length ==
2){
int lat =
Integer.
valueOf(tileCoords
[0]);
int lon =
Integer.
valueOf(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 =
true;
List<Way
> landWays =
new ArrayList<Way
>();
List<Way
> seaWays =
new ArrayList<Way
>();
// get the index with assignment key => sea/land/tilename
ZipFile zipFile =
null;
PrecompData pd = precompIndex.
get();
if (precompSeaDir.
getName().
endsWith(".zip")){
zipFile = pd.
zipFile;
}
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
Way w =
new Way
(FakeIdGenerator.
makeFakeId());
w.
addTag("natural", tileName
);
String[] tileCoords = keySplitter.
split(precompKey
);
int minLat =
Integer.
valueOf(tileCoords
[0]);
int minLon =
Integer.
valueOf(tileCoords
[1]);
int maxLat = minLat + PRECOMP_RASTER
;
int maxLon = minLon + PRECOMP_RASTER
;
w.
addPoint(new Coord
(minLat,minLon
));
w.
addPoint(new Coord
(minLat,maxLon
));
w.
addPoint(new Coord
(maxLat,maxLon
));
w.
addPoint(new Coord
(maxLat,minLon
));
w.
addPoint(new Coord
(minLat,minLon
));
if ("sea".
equals(tileName
)) {
seaWays.
add(w
);
} else {
landWays.
add(w
);
}
} else {
distinctTilesOnly =
false;
try {
InputStream is =
null;
if (zipFile
!=
null){
ZipEntry entry = zipFile.
getEntry(pd.
precompZipFileInternalPath + tileName
);
if (entry
!=
null){
is = zipFile.
getInputStream(entry
);
} else {
log.
error("Preompiled sea tile " + tileName +
" not found.");
}
} else {
File precompTile =
new File(precompSeaDir,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
) {
// set a new id to be sure that the precompiled ids do not
// interfere with the ids of this run
w.
setId(FakeIdGenerator.
makeFakeId());
if ("land".
equals(w.
getTag("natural"))) {
landWays.
add(w
);
} else {
seaWays.
add(w
);
}
}
}
} catch (FileNotFoundException exp
) {
log.
error("Preompiled sea tile " + tileName +
" not found.");
} catch (Exception exp
) {
log.
error(exp
);
exp.
printStackTrace();
}
}
}
// check if the land tags need to be changed
if (landTag
!=
null && ("natural".
equals(landTag
[0]) && "land".
equals(landTag
[1])) ==
false) {
for (Way w : landWays
) {
w.
deleteTag("natural");
w.
addTag(landTag
[0], landTag
[1]);
}
}
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 : landWays
) {
saver.
addWay(w
);
}
for (Way w : seaWays
) {
saver.
addWay(w
);
}
} else {
// using polygons
Area bounds = saver.
getBoundingBox();
// first add the complete bounding box as sea
Way sea =
new Way
(FakeIdGenerator.
makeFakeId());
sea.
addPoint(new Coord
(bounds.
getMinLat(), bounds.
getMinLong()));
sea.
addPoint(new Coord
(bounds.
getMinLat(), bounds.
getMaxLong()));
sea.
addPoint(new Coord
(bounds.
getMaxLat(), bounds.
getMaxLong()));
sea.
addPoint(new Coord
(bounds.
getMaxLat(), bounds.
getMinLong()));
sea.
addPoint(new Coord
(bounds.
getMinLat(), bounds.
getMinLong()));
sea.
addTag("natural",
"sea");
for (Way w : landWays
) {
saver.
addWay(w
);
}
}
}
/**
* 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 ArrayList<Way
> joinWays
(Collection<Way
> segments
) {
ArrayList<Way
> joined =
new ArrayList<Way
>((int)Math.
ceil(segments.
size()*0.5));
Map<Coord, Way
> beginMap =
new HashMap<Coord, Way
>();
for (Way w : segments
) {
if (w.
isClosed()) {
joined.
add(w
);
} else if (w.
getPoints() !=
null && w.
getPoints().
size() > 1){
List<Coord
> points = w.
getPoints();
beginMap.
put(points.
get(0), w
);
} else {
log.
info("Discard coastline way",w.
getId(),
"because consists of less than 2 points");
}
}
segments.
clear();
int merged =
1;
while (merged
> 0) {
merged =
0;
for (Way w1 : beginMap.
values()) {
if (w1.
isClosed()) {
// this should not happen
log.
error("joinWays2: Way "+w1+
" is closed but contained in the begin map");
joined.
add(w1
);
beginMap.
remove(w1.
getPoints().
get(0));
merged=
1;
break;
}
List<Coord
> points1 = w1.
getPoints();
Way w2 = beginMap.
get(points1.
get(points1.
size() -
1));
if (w2
!=
null) {
log.
info("merging: ", beginMap.
size(), w1.
getId(),
w2.
getId());
List<Coord
> points2 = w2.
getPoints();
Way wm
;
if (FakeIdGenerator.
isFakeId(w1.
getId())) {
wm = w1
;
} else {
wm =
new Way
(FakeIdGenerator.
makeFakeId());
wm.
getPoints().
addAll(points1
);
beginMap.
put(points1.
get(0), wm
);
}
wm.
getPoints().
addAll(points2.
subList(1, points2.
size()));
beginMap.
remove(points2.
get(0));
merged++
;
if (wm.
isClosed()) {
joined.
add(wm
);
beginMap.
remove(wm.
getPoints().
get(0));
}
break;
}
}
}
log.
info(joined.
size(),
"closed ways.",beginMap.
size(),
"unclosed ways.");
joined.
addAll(beginMap.
values());
return joined
;
}
/**
* All done, process the saved shoreline information and construct the polygons.
*/
public void end
() {
// precompiled sea has highest priority
// if it is set do not perform any other algorithm
if (precompSeaDir
!=
null) {
addPrecompSea
();
return;
}
Area seaBounds = saver.
getBoundingBox();
if (coastlineFilenames ==
null) {
log.
info("Shorelines before join", shoreline.
size());
shoreline = joinWays
(shoreline
);
} else {
shoreline.
addAll(CoastlineFileLoader.
getCoastlineLoader()
.
getCoastlines(seaBounds
));
log.
info("Shorelines from extra file:", shoreline.
size());
}
int closedS =
0;
int unclosedS =
0;
for (Way w : shoreline
) {
if (w.
isClosed()) {
closedS++
;
} else {
unclosedS++
;
}
}
log.
info("Closed shorelines", closedS
);
log.
info("Unclosed shorelines", unclosedS
);
// clip all shoreline segments
clipShorlineSegments
(shoreline, seaBounds
);
log.
info("generating sea, seaBounds=", seaBounds
);
int minLat = seaBounds.
getMinLat();
int maxLat = seaBounds.
getMaxLat();
int minLong = seaBounds.
getMinLong();
int maxLong = seaBounds.
getMaxLong();
Coord nw =
new Coord
(minLat, minLong
);
Coord ne =
new Coord
(minLat, maxLong
);
Coord sw =
new Coord
(maxLat, minLong
);
Coord se =
new Coord
(maxLat, maxLong
);
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
long landId = FakeIdGenerator.
makeFakeId();
Way land =
new Way
(landId
);
land.
addPoint(nw
);
land.
addPoint(sw
);
land.
addPoint(se
);
land.
addPoint(ne
);
land.
addPoint(nw
);
land.
addTag(landTag
[0], landTag
[1]);
// no matter if the multipolygon option is used it is
// only necessary to create a land polygon
saver.
addWay(land
);
// nothing more to do
return;
}
long multiId = FakeIdGenerator.
makeFakeId();
Relation seaRelation =
null;
if(generateSeaUsingMP
) {
log.
debug("Generate seabounds relation",multiId
);
seaRelation =
new GeneralRelation
(multiId
);
seaRelation.
addTag("type",
"multipolygon");
seaRelation.
addTag("natural",
"sea");
}
List<Way
> islands =
new ArrayList<Way
>();
// handle islands (closed shoreline components) first (they're easy)
handleIslands
(shoreline, seaBounds, islands
);
// the remaining shoreline segments should intersect the boundary
// find the intersection points and store them in a SortedMap
NavigableMap
<EdgeHit, Way
> hitMap = findIntesectionPoints
(shoreline, seaBounds, seaRelation
);
// now construct inner ways from these segments
boolean shorelineReachesBoundary = createInnerWays
(seaBounds, islands, hitMap
);
if(!shorelineReachesBoundary
&& roadsReachBoundary
) {
// try to avoid tiles being flooded by anti-lakes or other
// bogus uses of natural=coastline
generateSeaBackground =
false;
}
List<Way
> antiIslands = removeAntiIslands
(seaRelation, islands
);
if (islands.
isEmpty()) {
// the tile doesn't contain any islands so we can assume
// that it's showing a land mass that contains some
// enclosed sea areas - in which case, we don't want a sea
// coloured background
generateSeaBackground =
false;
}
if (generateSeaBackground
) {
// the background is sea so all anti-islands should be
// contained by land otherwise they won't be visible
for (Way ai : antiIslands
) {
boolean containedByLand =
false;
for(Way i : islands
) {
if(i.
containsPointsOf(ai
)) {
containedByLand =
true;
break;
}
}
if (!containedByLand
) {
// found an anti-island that is not contained by
// land so convert it back into an island
ai.
deleteTag("natural");
ai.
addTag(landTag
[0], landTag
[1]);
if (generateSeaUsingMP
) {
// create a "inner" way for the island
assert seaRelation
!=
null;
seaRelation.
addElement("inner", ai
);
}
log.
warn("Converting anti-island starting at", ai.
getPoints().
get(0).
toOSMURL() ,
"into an island as it is surrounded by water");
}
}
long seaId = FakeIdGenerator.
makeFakeId();
Way sea =
new Way
(seaId
);
// the sea background area must be a little bigger than all
// inner land areas. this is a workaround for a mp shortcoming:
// mp is not able to combine outer and inner if they intersect
// or have overlaying lines
// the added area will be clipped later by the style generator
sea.
addPoint(new Coord
(nw.
getLatitude() -
1,
nw.
getLongitude() -
1));
sea.
addPoint(new Coord
(sw.
getLatitude() +
1,
sw.
getLongitude() -
1));
sea.
addPoint(new Coord
(se.
getLatitude() +
1,
se.
getLongitude() +
1));
sea.
addPoint(new Coord
(ne.
getLatitude() -
1,
ne.
getLongitude() +
1));
sea.
addPoint(new Coord
(nw.
getLatitude() -
1,
nw.
getLongitude() -
1));
sea.
addTag("natural",
"sea");
log.
info("sea: ", sea
);
saver.
addWay(sea
);
if(generateSeaUsingMP
) {
assert seaRelation
!=
null;
seaRelation.
addElement("outer", sea
);
}
} else {
// background is land
// generate a land polygon so that the tile's
// background colour will match the land colour on the
// tiles that do contain some sea
long landId = FakeIdGenerator.
makeFakeId();
Way land =
new Way
(landId
);
land.
addPoint(nw
);
land.
addPoint(sw
);
land.
addPoint(se
);
land.
addPoint(ne
);
land.
addPoint(nw
);
land.
addTag(landTag
[0], landTag
[1]);
saver.
addWay(land
);
if (generateSeaUsingMP
) {
seaRelation.
addElement("inner", land
);
}
}
if (generateSeaUsingMP
) {
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;
}
/**
* Clip the shoreline ways to the bounding box of the map.
* @param shoreline All the the ways making up the coast.
* @param bounds The map bounds.
*/
private void clipShorlineSegments
(List<Way
> shoreline,
Area bounds
) {
List<Way
> toBeRemoved =
new ArrayList<Way
>();
List<Way
> toBeAdded =
new ArrayList<Way
>();
for (Way segment : shoreline
) {
List<Coord
> points = segment.
getPoints();
List<List<Coord
>> clipped = LineClipper.
clip(bounds, points
);
if (clipped
!=
null) {
log.
info("clipping", segment
);
toBeRemoved.
add(segment
);
for (List<Coord
> pts : clipped
) {
long id = FakeIdGenerator.
makeFakeId();
Way shore =
new Way
(id, pts
);
toBeAdded.
add(shore
);
}
}
}
log.
info("clipping: adding", toBeAdded.
size(),
", removing", toBeRemoved.
size());
shoreline.
removeAll(toBeRemoved
);
shoreline.
addAll(toBeAdded
);
}
/**
* Pick out the islands and save them for later. They are removed from the
* shore line list and added to the island list.
*
* @param shoreline The collected shore line ways.
* @param seaBounds The map boundary.
* @param islands The islands are saved to this list.
*/
private void handleIslands
(List<Way
> shoreline,
Area seaBounds,
List<Way
> islands
) {
Iterator<Way
> it = shoreline.
iterator();
while (it.
hasNext()) {
Way w = it.
next();
if (w.
isClosed()) {
log.
info("adding island", w
);
islands.
add(w
);
it.
remove();
}
}
closeGaps
(shoreline, seaBounds
);
// there may be more islands now
it = shoreline.
iterator();
while (it.
hasNext()) {
Way w = it.
next();
if (w.
isClosed()) {
log.
debug("island after concatenating");
islands.
add(w
);
it.
remove();
}
}
}
private boolean createInnerWays
(Area seaBounds,
List<Way
> islands, NavigableMap
<EdgeHit, Way
> hitMap
) {
NavigableSet
<EdgeHit
> hits = hitMap.
navigableKeySet();
boolean shorelineReachesBoundary =
false;
while (!hits.
isEmpty()) {
long id = FakeIdGenerator.
makeFakeId();
Way w =
new Way
(id
);
saver.
addWay(w
);
EdgeHit hit = hits.
first();
EdgeHit hFirst = hit
;
do {
Way segment = hitMap.
get(hit
);
log.
info("current hit:", hit
);
EdgeHit hNext
;
if (segment
!=
null) {
// add the segment and get the "ending hit"
log.
info("adding:", segment
);
for(Coord p : segment.
getPoints())
w.
addPointIfNotEqualToLastPoint(p
);
hNext = getEdgeHit
(seaBounds, segment.
getPoints().
get(segment.
getPoints().
size()-
1));
} else {
w.
addPointIfNotEqualToLastPoint(hit.
getPoint(seaBounds
));
hNext = hits.
higher(hit
);
if (hNext ==
null)
hNext = hFirst
;
Coord p
;
if (hit.
compareTo(hNext
) < 0) {
log.
info("joining: ", hit, hNext
);
for (int i=hit.
edge; i
<hNext.
edge; i++
) {
EdgeHit corner =
new EdgeHit
(i,
1.0);
p = corner.
getPoint(seaBounds
);
log.
debug("way: ", corner, p
);
w.
addPointIfNotEqualToLastPoint(p
);
}
} else if (hit.
compareTo(hNext
) > 0) {
log.
info("joining: ", hit, hNext
);
for (int i=hit.
edge; i
<4; i++
) {
EdgeHit corner =
new EdgeHit
(i,
1.0);
p = corner.
getPoint(seaBounds
);
log.
debug("way: ", corner, p
);
w.
addPointIfNotEqualToLastPoint(p
);
}
for (int i=
0; i
<hNext.
edge; i++
) {
EdgeHit corner =
new EdgeHit
(i,
1.0);
p = corner.
getPoint(seaBounds
);
log.
debug("way: ", corner, p
);
w.
addPointIfNotEqualToLastPoint(p
);
}
}
w.
addPointIfNotEqualToLastPoint(hNext.
getPoint(seaBounds
));
}
hits.
remove(hit
);
hit = hNext
;
} while (!hits.
isEmpty() && !hit.
equals(hFirst
));
if (!w.
isClosed())
w.
getPoints().
add(w.
getPoints().
get(0));
log.
info("adding non-island landmass, hits.size()=" + hits.
size());
islands.
add(w
);
shorelineReachesBoundary =
true;
}
return shorelineReachesBoundary
;
}
/**
* An 'anti-island' is something that has been detected as an island, but the water
* is on the inside. I think you would call this a lake.
* @param seaRelation The relation holding the sea. Only set if we are using multi-polygons for
* the sea.
* @param islands The island list that was found earlier.
* @return The so-called anti-islands.
*/
private List<Way
> removeAntiIslands
(Relation seaRelation,
List<Way
> islands
) {
List<Way
> antiIslands =
new ArrayList<Way
>();
for (Way w : islands
) {
if (!FakeIdGenerator.
isFakeId(w.
getId())) {
Way w1 =
new Way
(FakeIdGenerator.
makeFakeId());
w1.
getPoints().
addAll(w.
getPoints());
// only copy the name tags
for(String tag : w
)
if(tag.
equals("name") || tag.
endsWith(":name"))
w1.
addTag(tag, w.
getTag(tag
));
w = w1
;
}
// determine where the water is
if (Way.
clockwise(w.
getPoints())) {
// water on the inside of the poly, it's an
// "anti-island" so tag with natural=water (to
// make it visible above the land)
w.
addTag("natural",
"water");
antiIslands.
add(w
);
saver.
addWay(w
);
} else {
// water on the outside of the poly, it's an island
w.
addTag(landTag
[0], landTag
[1]);
saver.
addWay(w
);
if(generateSeaUsingMP
) {
// create a "inner" way for each island
seaRelation.
addElement("inner", w
);
}
}
}
islands.
removeAll(antiIslands
);
return antiIslands
;
}
/**
* Find the points where the remaining shore line segments intersect with the
* map boundary.
*
* @param shoreline The remaining shore line segments.
* @param seaBounds The map boundary.
* @param seaRelation If we are using a multi-polygon, this is it. Otherwise it will be null.
* @return A map of the 'hits' where the shore line intersects the boundary.
*/
private NavigableMap
<EdgeHit, Way
> findIntesectionPoints
(List<Way
> shoreline,
Area seaBounds,
Relation seaRelation
) {
assert !generateSeaUsingMP || seaRelation
!=
null;
NavigableMap
<EdgeHit, Way
> hitMap =
new TreeMap<EdgeHit, Way
>();
for (Way w : shoreline
) {
List<Coord
> points = w.
getPoints();
Coord pStart = points.
get(0);
Coord pEnd = points.
get(points.
size()-
1);
EdgeHit hStart = getEdgeHit
(seaBounds, pStart
);
EdgeHit hEnd = getEdgeHit
(seaBounds, pEnd
);
if (hStart ==
null || hEnd ==
null) {
/*
* This problem occurs usually when the shoreline is cut by osmosis (e.g. country-extracts from geofabrik)
* There are two possibilities to solve this problem:
* 1. Close the way and treat it as an island. This is sometimes the best solution (Germany: Usedom at the
* border to Poland)
* 2. Create a "sea sector" only for this shoreline segment. This may also be the best solution
* (see German border to the Netherlands where the shoreline continues in the Netherlands)
* The first choice may lead to "flooded" areas, the second may lead to "triangles".
*
* Usually, the first choice is appropriate if the segment is "nearly" closed.
*/
double length =
0;
Coord p0 = pStart
;
for (Coord p1 : points.
subList(1, points.
size()-
1)) {
length += p0.
distance(p1
);
p0 = p1
;
}
boolean nearlyClosed = pStart.
distance(pEnd
) < 0.1 * length
;
if (nearlyClosed
) {
// close the way
points.
add(pStart
);
if(!FakeIdGenerator.
isFakeId(w.
getId())) {
Way w1 =
new Way
(FakeIdGenerator.
makeFakeId());
w1.
getPoints().
addAll(w.
getPoints());
// only copy the name tags
for(String tag : w
)
if(tag.
equals("name") || tag.
endsWith(":name"))
w1.
addTag(tag, w.
getTag(tag
));
w = w1
;
}
w.
addTag(landTag
[0], landTag
[1]);
saver.
addWay(w
);
if(generateSeaUsingMP
)
{
seaRelation.
addElement("inner", w
);
}
} else if(allowSeaSectors
) {
long seaId = FakeIdGenerator.
makeFakeId();
Way sea =
new Way
(seaId
);
sea.
getPoints().
addAll(points
);
sea.
addPoint(new Coord
(pEnd.
getLatitude(), pStart.
getLongitude()));
sea.
addPoint(pStart
);
sea.
addTag("natural",
"sea");
log.
info("sea: ", sea
);
saver.
addWay(sea
);
if(generateSeaUsingMP
)
seaRelation.
addElement("outer", sea
);
generateSeaBackground =
false;
} else if (extendSeaSectors
) {
// create additional points at next border to prevent triangles from point 2
if (null == hStart
) {
hStart = getNextEdgeHit
(seaBounds, pStart
);
w.
getPoints().
add(0, hStart.
getPoint(seaBounds
));
}
if (null == hEnd
) {
hEnd = getNextEdgeHit
(seaBounds, pEnd
);
w.
getPoints().
add(hEnd.
getPoint(seaBounds
));
}
log.
debug("hits (second try): ", hStart, hEnd
);
hitMap.
put(hStart, w
);
hitMap.
put(hEnd,
null);
} else {
// show the coastline even though we can't produce
// a polygon for the land
w.
addTag("natural",
"coastline");
saver.
addWay(w
);
}
} else {
log.
debug("hits: ", hStart, hEnd
);
hitMap.
put(hStart, w
);
hitMap.
put(hEnd,
null);
}
}
return hitMap
;
}
/**
* Specifies where an edge of the bounding box is hit.
*/
private static class EdgeHit
implements Comparable<EdgeHit
>
{
private final int edge
;
private final double t
;
EdgeHit
(int edge,
double t
) {
this.
edge = edge
;
this.
t = t
;
}
public int compareTo
(EdgeHit o
) {
if (edge
< o.
edge)
return -
1;
else if (edge
> o.
edge)
return +
1;
else if (t
> o.
t)
return +
1;
else if (t
< o.
t)
return -
1;
else
return 0;
}
public boolean equals
(Object o
) {
if (o
instanceof EdgeHit
) {
EdgeHit h =
(EdgeHit
) o
;
return (h.
edge == edge
&& Double.
compare(h.
t, t
) ==
0);
} else
return false;
}
private Coord getPoint
(Area a
) {
log.
info("getPoint: ",
this, a
);
switch (edge
) {
case 0:
return new Coord
(a.
getMinLat(),
(int) (a.
getMinLong() + t
* (a.
getMaxLong()-a.
getMinLong())));
case 1:
return new Coord
((int)(a.
getMinLat() + t
* (a.
getMaxLat()-a.
getMinLat())), a.
getMaxLong());
case 2:
return new Coord
(a.
getMaxLat(),
(int)(a.
getMaxLong() - t
* (a.
getMaxLong()-a.
getMinLong())));
case 3:
return new Coord
((int)(a.
getMaxLat() - t
* (a.
getMaxLat()-a.
getMinLat())), a.
getMinLong());
default:
throw new MapFailedException
("illegal state");
}
}
public String toString
() {
return "EdgeHit " + edge +
"@" + t
;
}
}
private EdgeHit getEdgeHit
(Area a, Coord p
) {
return getEdgeHit
(a, p,
10);
}
private EdgeHit getEdgeHit
(Area a, Coord p,
int tolerance
) {
int lat = p.
getLatitude();
int lon = p.
getLongitude();
int minLat = a.
getMinLat();
int maxLat = a.
getMaxLat();
int minLong = a.
getMinLong();
int maxLong = a.
getMaxLong();
log.
info(String.
format("getEdgeHit: (%d %d) (%d %d %d %d)", lat, lon, minLat, minLong, maxLat, maxLong
));
if (lat
<= minLat+tolerance
) {
return new EdgeHit
(0,
((double)(lon - minLong
))/
(maxLong-minLong
));
} else if (lon
>= maxLong-tolerance
) {
return new EdgeHit
(1,
((double)(lat - minLat
))/
(maxLat-minLat
));
} else if (lat
>= maxLat-tolerance
) {
return new EdgeHit
(2,
((double)(maxLong - lon
))/
(maxLong-minLong
));
} else if (lon
<= minLong+tolerance
) {
return new EdgeHit
(3,
((double)(maxLat - lat
))/
(maxLat-minLat
));
} else
return null;
}
/**
* Find the nearest edge for supplied Coord p.
*/
private EdgeHit getNextEdgeHit
(Area a, Coord p
)
{
int lat = p.
getLatitude();
int lon = p.
getLongitude();
int minLat = a.
getMinLat();
int maxLat = a.
getMaxLat();
int minLong = a.
getMinLong();
int maxLong = a.
getMaxLong();
log.
info(String.
format("getNextEdgeHit: (%d %d) (%d %d %d %d)", lat, lon, minLat, minLong, maxLat, maxLong
));
// shortest distance to border (init with distance to southern border)
int min = lat - minLat
;
// number of edge as used in getEdgeHit.
// 0 = southern
// 1 = eastern
// 2 = northern
// 3 = western edge of Area a
int i =
0;
// normalized position at border (0..1)
double l =
((double)(lon - minLong
))/
(maxLong-minLong
);
// now compare distance to eastern border with already known distance
if (maxLong - lon
< min
) {
// update data if distance is shorter
min = maxLong - lon
;
i =
1;
l =
((double)(lat - minLat
))/
(maxLat-minLat
);
}
// same for northern border
if (maxLat - lat
< min
) {
min = maxLat - lat
;
i =
2;
l =
((double)(maxLong - lon
))/
(maxLong-minLong
);
}
// same for western border
if (lon - minLong
< min
) {
i =
3;
l =
((double)(maxLat - lat
))/
(maxLat-minLat
);
}
// now created the EdgeHit for found values
return new EdgeHit
(i, l
);
}
private void closeGaps
(List<Way
> ways,
Area bounds
) {
// join up coastline segments whose end points are less than
// maxCoastlineGap metres apart
if (maxCoastlineGap
> 0) {
boolean changed =
true;
while (changed
) {
changed =
false;
for (Way w1 : ways
) {
if(w1.
isClosed())
continue;
List<Coord
> points1 = w1.
getPoints();
Coord w1e = points1.
get(points1.
size() -
1);
if(bounds.
onBoundary(w1e
))
continue;
Way nearest =
null;
double smallestGap =
Double.
MAX_VALUE;
for (Way w2 : ways
) {
if(w1 == w2 || w2.
isClosed())
continue;
List<Coord
> points2 = w2.
getPoints();
Coord w2s = points2.
get(0);
if(bounds.
onBoundary(w2s
))
continue;
double gap = w1e.
distance(w2s
);
if(gap
< smallestGap
) {
nearest = w2
;
smallestGap = gap
;
}
}
if (nearest
!=
null && smallestGap
< maxCoastlineGap
) {
Coord w2s = nearest.
getPoints().
get(0);
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
(FakeIdGenerator.
makeFakeId());
ways.
remove(w1
);
ways.
add(wm
);
wm.
getPoints().
addAll(points1
);
wm.
copyTags(w1
);
}
wm.
getPoints().
addAll(nearest.
getPoints());
ways.
remove(nearest
);
// make a line that shows the filled gap
Way w =
new Way
(FakeIdGenerator.
makeFakeId());
w.
addTag("natural",
"mkgmap:coastline-gap");
w.
addPoint(w1e
);
w.
addPoint(w2s
);
saver.
addWay(w
);
changed =
true;
break;
}
}
}
}
}
/**
* Helper class for threadlocal vars
*
*
*/
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
;
}
}