/**
* This file is part of OSM2ShareNav
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
* See COPYING.
*
* Copyright (c) 2010 Steve Ratcliffe, Markus Baeurle
* Ported from the mkgmap project, class Osm5XmlHandler.
*/
package net.sharenav.osmToShareNav.area;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.SortedMap;
import java.util.TreeMap;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.LineClipper;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import net.sharenav.osmToShareNav.Configuration;
import net.sharenav.osmToShareNav.MyMath;
import net.sharenav.osmToShareNav.model.Bounds;
import net.sharenav.osmToShareNav.model.Node;
import net.sharenav.osmToShareNav.model.Relation;
import net.sharenav.osmToShareNav.model.Tile;
import net.sharenav.osmToShareNav.model.Way;
/**
* Static class which checks if there are coast lines in the tile which need to be
* extended to a sea polygon.
*/
public class SeaGenerator {
private static final Logger log = Logger.getLogger(SeaGenerator.class);
private static final String[] landTag = { "natural", "generated-land" };
private static boolean generateSea = true;
private static boolean generateSeaUsingMP = false;
private static boolean allowSeaSectors = false;
private static boolean extendSeaSectors = true;
private static int maxCoastlineGap = 100;
private static Configuration configuration;
/**
* Set various options for generating sea areas
* @param aGenerateSea Generate sea areas at all
* @param aGenerateSeaUsingMP Use multi polygons for the sea areas
* @param aAllowSeaSectors Allow to create new ways for the sea area,
* this excludes extendSeaSectors = true.
* @param aExtendSeaSectors Allow to extend ways to form the sea area
* @param aMaxCoastlineGap Gaps below this length (in meters) in coast line will be filled.
*/
public static void setOptions(Configuration conf, boolean aGenerateSea,
boolean aGenerateSeaUsingMP, boolean aAllowSeaSectors,
boolean aExtendSeaSectors, int aMaxCoastlineGap) {
configuration = conf;
generateSea = aGenerateSea;
generateSeaUsingMP = aGenerateSeaUsingMP;
allowSeaSectors = aAllowSeaSectors;
extendSeaSectors = aExtendSeaSectors;
maxCoastlineGap = aMaxCoastlineGap;
}
/**
* Checks for coast lines and creates one (or more) sea areas which extend(s) to
* the boundaries of this tile.
*
* @param tile Tile to work on
*/
public static void generateSeaPolygon(Tile tile) {
if (generateSea == false) {
return;
}
// Bounds of tile are always initialized.
Bounds seaBounds = tile.getBounds();
// Pick out all coast line ways. This is from Osm5XmlHandler.endElement()
List<Way> shoreline = new ArrayList<Way>();
String natural;
for (Way currentWay : tile.ways) {
natural = currentWay.getAttribute("natural");
if (natural != null) {
if ("coastline".equals(natural)) {
// FIXME should delete the tag
//currentWay.deleteTag("natural");
shoreline.add(currentWay);
}
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) {
//currentWay.deleteTag("natural");
if (others != null) {
currentWay.setAttribute("natural", others);
}
shoreline.add(currentWay);
}
}
}
} // for
log.info("generating sea, seaBounds=", seaBounds);
float minLat = seaBounds.getMinLat();
float maxLat = seaBounds.getMaxLat();
float minLong = seaBounds.getMinLon();
float maxLong = seaBounds.getMaxLon();
Node nw = new Node(minLat, minLong, FakeIdGenerator.makeFakeId());
Node ne = new Node(minLat, maxLong, FakeIdGenerator.makeFakeId());
Node sw = new Node(maxLat, minLong, FakeIdGenerator.makeFakeId());
Node se = new Node(maxLat, maxLong, FakeIdGenerator.makeFakeId());
if(shoreline.isEmpty()) {
// no sea required
/* TODO: Cleanup after decision about background
if(!generateSeaUsingMP) {
// 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.addNode(nw);
land.addNode(sw);
land.addNode(se);
land.addNode(ne);
land.addNode(nw);
land.setAttribute(landTag[0], landTag[1]);
tile.addWay(land);
}
*/
// nothing more to do
return;
}
// Clip all shoreline segments
// Moved this down because it only makes sense if there are coast lines at all.
// TODO: Do we need this in Osm2ShareNav or aren't the ways already clipped?
// Apparently yes: otherwise there will be loads of messages like
/*
===============================
2010/12/28 23:33:01 WARNING (SeaGenerator): Non-closed coastline segment does not hit bounding box: start id=248533560 (60.173336|24.822487) name=null end id=25197279 (60.173588|24.829788) name=null
See http://www.openstreetmap.org/browse/node/248533560 and http://www.openstreetmap.org/browse/node/25197279
*/
List<Way> toBeRemoved = new ArrayList<Way>();
List<Way> toBeAdded = new ArrayList<Way>();
for (Way segment : shoreline) {
List<Node> points = segment.getNodes();
List<ArrayList<Node>> clipped = LineClipper.clip(seaBounds, points);
//List<List<Node>> clipped = null;
if (clipped != null) {
log.info("clipping " + segment);
toBeRemoved.add(segment);
for (ArrayList<Node> 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);
long multiId = FakeIdGenerator.makeFakeId();
Relation seaRelation = null;
if(generateSeaUsingMP) {
log.error("Sea multipolygons are not supported yet!");
/* log.debug("Generate seabounds relation "+multiId);
seaRelation = new Relation(multiId);
seaRelation.setAttribute("type", "multipolygon");
seaRelation.setAttribute("natural", "sea");
*/
}
List<Way> islands = new ArrayList<Way>();
// handle islands (closes shoreline components) first (they're easy)
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();
}
}
concatenateWays(shoreline, seaBounds, tile);
// there may be more islands now
it = shoreline.iterator();
while (it.hasNext()) {
Way w = it.next();
if (w.isClosed()) {
log.debug("island after concatenating\n");
islands.add(w);
it.remove();
}
}
boolean generateSeaBackground = true;
// the remaining shoreline segments should intersect the boundary
// find the intersection points and store them in a SortedMap
SortedMap<EdgeHit, Way> hitMap = new TreeMap<EdgeHit, Way>();
long seaId;
Way sea;
for (Way w : shoreline) {
List<Node> points = w.getNodes();
Node pStart = points.get(0);
Node pEnd = points.get(points.size()-1);
EdgeHit hStart = getEdgeHit(seaBounds, pStart);
EdgeHit hEnd = getEdgeHit(seaBounds, pEnd);
if (hStart == null || hEnd == null) {
String msg = String.format(
"Non-closed coastline segment does not hit bounding box: start %s end %s\n" +
" See %s and %s\n",
pStart.toString(), pEnd.toString(),
pStart.toUrl(), pEnd.toUrl());
log.warn(msg);
System.out.println(msg);
/*
* 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;
Node p0 = pStart;
for (Node p1 : points.subList(1, points.size()-1)) {
length += MyMath.dist(p0, p1);
p0 = p1;
}
boolean nearlyClosed = (MyMath.dist(pStart, pEnd) < 0.1 * length);
if (nearlyClosed) {
// close the way
points.add(pStart);
if(generateSeaUsingMP) {
// TODO seaRelation.addElement("inner", w);
} else {
if(!FakeIdGenerator.isFakeId(w.getId())) {
Way w1 = new Way(FakeIdGenerator.makeFakeId());
w1.getNodes().addAll(w.getNodes());
// only copy the name tags
for(String tag : w.getTags()) {
if(tag.equals("name") || tag.endsWith(":name")) {
w1.setAttribute(tag, w.getAttribute(tag));
}
}
w = w1;
}
w.setAttribute(landTag[0], landTag[1]);
w.setAttribute("area", "yes");
/* This line is not unnecessary as it triggers the calculation of the way's type
* (which is why making this method sound like it's a simple getter is
* a bloody bad idea).
*/
// polish.api.bigstyles
short t = w.getType(configuration);
tile.addWay(w);
}
}
else if(allowSeaSectors) {
System.out.println("in allowSeaSectors");
seaId = FakeIdGenerator.makeFakeId();
sea = new Way(seaId);
sea.getNodes().addAll(points);
//sea.addNode(new Node(pEnd.getLat(), pStart.getLon(),
// FakeIdGenerator.makeFakeId()));
// FIXME seems to only work for one tile
Node p;
int startedge = 0;
int endedge = 3;
EdgeHit startEdgeHit = getNextEdgeHit(seaBounds, pStart) ;
EdgeHit endEdgeHit = getNextEdgeHit(seaBounds, pEnd) ;
startedge = startEdgeHit.edge;
endedge = endEdgeHit.edge;
System.out.println("startedge: " + startedge + " endedge: " + endedge);
if (endedge < startedge) {
endedge += 4;
}
for (int i=endedge; i > startedge; i--) {
int edge = i % 4;
float val = 0.0f;
System.out.println("edge: " + edge + " val: " + val);
EdgeHit corner = new EdgeHit(edge, val);
p = corner.getPoint(seaBounds);
log.debug("way: ", corner, p);
System.out.println("way: corner: " + corner + " p: " + p);
sea.addNodeIfNotEqualToLastNode(p);
}
sea.addNode(pStart);
// TODO: Is natural=sea good to get it shown?
// Is something else (colours?) needed for Osm2ShareNav?
sea.setAttribute("natural", "sea");
sea.setAttribute("area", "yes");
/* This line is not unnecessary as it triggers the calculation of the way's type
* (which is why making this method sound like it's a simple getter is
* a bloody bad idea).
*/
// polish.api.bigstyles
short t = sea.getType(configuration);
log.info("sea (newly created): ", sea);
System.out.println(" Sea (newly created): " + sea);
tile.addWay(sea);
if(generateSeaUsingMP) {
// TODO 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.getNodes().add(0, hStart.getPoint(seaBounds));
}
if (null == hEnd) {
hEnd = getNextEdgeHit(seaBounds, pEnd);
w.getNodes().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.setAttribute("natural", "coastline");
tile.addWay(w);
}
}
else {
log.debug("hits: ", hStart, hEnd);
hitMap.put(hStart, w);
hitMap.put(hEnd, null);
}
}
// now construct inner ways from these segments
NavigableSet<EdgeHit> hits = (NavigableSet<EdgeHit>) hitMap.keySet();
boolean shorelineReachesBoundary = false;
while (!hits.isEmpty()) {
long id = FakeIdGenerator.makeFakeId();
Way w = new Way(id);
tile.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(Node p : segment.getNodes()) {
w.addNodeIfNotEqualToLastNode(p);
}
hNext = getEdgeHit(seaBounds, segment.getNodes().get(segment.getNodes().size()-1));
}
else {
w.addNodeIfNotEqualToLastNode(hit.getPoint(seaBounds));
hNext = hits.higher(hit);
if (hNext == null) {
hNext = hFirst;
}
Node 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.addNodeIfNotEqualToLastNode(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.addNodeIfNotEqualToLastNode(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.addNodeIfNotEqualToLastNode(p);
}
}
w.addNodeIfNotEqualToLastNode(hNext.getPoint(seaBounds));
}
hits.remove(hit);
hit = hNext;
} while (!hits.isEmpty() && !hit.equals(hFirst));
if (!w.isClosed()) {
w.getNodes().add(w.getNodes().get(0));
}
log.info("adding non-island landmass, hits.size()=" + hits.size());
islands.add(w);
shorelineReachesBoundary = true;
}
// TODO roadsReachBoundary if(!shorelineReachesBoundary && roadsReachBoundary) {
// try to avoid tiles being flooded by anti-lakes or other
// bogus uses of natural=coastline
//generateSeaBackground = false;
//}
List<Way> antiIslands = new ArrayList<Way>();
for (Way w : islands) {
if(!FakeIdGenerator.isFakeId(w.getId())) {
Way w1 = new Way(FakeIdGenerator.makeFakeId());
w1.getNodes().addAll(w.getNodes());
// only copy the name tags
for(String tag : w.getTags()) {
if(tag.equals("name") || tag.endsWith(":name")) {
w1.setAttribute(tag, w.getAttribute(tag));
}
}
w = w1;
}
// determine where the water is
if(w.isClockwise()) {
// 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.setAttribute("natural", "water");
antiIslands.add(w);
tile.addWay(w);
}
else {
// water on the outside of the poly, it's an island
if(generateSeaUsingMP) {
// create a "inner" way for each island
// TODO seaRelation.addElement("inner", w);
}
else {
// tag as land
w.setAttribute(landTag[0], landTag[1]);
w.setAttribute("area", "yes");
/* This line is not unnecessary as it triggers the calculation of the way's type
* (which is why making this method sound like it's a simple getter is
* a bloody bad idea).
*/
// polish.api.bigstyles
short t = w.getType(configuration);
tile.addWay(w);
}
}
}
islands.removeAll(antiIslands);
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");
if(generateSeaUsingMP) {
// create a "inner" way for the island
// TODO seaRelation.addElement("inner", ai);
// TODO tile.removeWay(ai.getId());
} else {
ai.setAttribute(landTag[0], landTag[1]);
}
log.warn("Converting anti-island starting at " +
ai.getNodes().get(0).toUrl() +
" into an island as it is surrounded by water");
}
}
seaId = FakeIdGenerator.makeFakeId();
sea = new Way(seaId);
if (generateSeaUsingMP) {
// 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
// mkgmap uses ints for lat/lon where a digit is 1 / (2^24) of a degree
// (see Utils.toMapUnit()). So a value of 1 is 0.0000214576721191 degrees
// or about 0.077 arc seconds.
sea.addNode(new Node(nw.getLat() - 0.00002f,
nw.getLon() - 0.00002f, FakeIdGenerator.makeFakeId()));
sea.addNode(new Node(sw.getLat() + 0.00002f,
sw.getLon() - 0.00002f, FakeIdGenerator.makeFakeId()));
sea.addNode(new Node(se.getLat() + 0.00002f,
se.getLon() + 0.00002f, FakeIdGenerator.makeFakeId()));
sea.addNode(new Node(ne.getLat() - 0.00002f,
ne.getLon() + 0.00002f, FakeIdGenerator.makeFakeId()));
sea.addNode(new Node(nw.getLat() - 0.00002f,
nw.getLon() - 0.00002f, FakeIdGenerator.makeFakeId()));
} else {
sea.addNode(nw);
sea.addNode(sw);
sea.addNode(se);
sea.addNode(ne);
sea.addNode(nw);
}
sea.setAttribute("natural", "sea");
sea.setAttribute("area", "yes");
/* This line is not unnecessary as it triggers the calculation of the way's type
* (which is why making this method sound like it's a simple getter is
* a bloody bad idea).
*/
// polish.api.bigstyles
short t = sea.getType(configuration);
log.info("sea background: ", sea);
System.out.println(" Sea background: " + sea);
tile.addWay(sea);
if(generateSeaUsingMP) {
// TODO seaRelation.addElement("outer", sea);
}
}
else {
// background is land
if(!generateSeaUsingMP) {
// 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.addNode(nw);
land.addNode(sw);
land.addNode(se);
land.addNode(ne);
land.addNode(nw);
land.setAttribute(landTag[0], landTag[1]);
land.setAttribute("area", "yes");
/* This line is not unnecessary as it triggers the calculation of the way's type
* (which is why making this method sound like it's a simple getter is
* a bloody bad idea).
*/
// polish.api.bigstyles
short t = land.getType(configuration);
tile.addWay(land);
}
}
if (generateSeaUsingMP) {
Bounds mpBbox = tile.getBounds();
// TODO seaRelation = new MultiPolygonRelation(seaRelation, wayMap, mpWayRemoveTags, mpBbox);
//relationMap.put(multiId, seaRelation);
//seaRelation.processElements();
}
}
/**
* 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;
}
}
@Override
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 Node getPoint(Bounds a) {
log.info("getPoint: ", this, a);
switch (edge) {
case 0:
return new Node(a.getMinLat(),
(float)(a.getMinLon() + t * (a.getMaxLon() - a.getMinLon())),
FakeIdGenerator.makeFakeId());
case 1:
return new Node((float)(a.getMinLat() + t * (a.getMaxLat() - a.getMinLat())),
a.getMaxLon(), FakeIdGenerator.makeFakeId());
case 2:
return new Node(a.getMaxLat(),
(float)(a.getMaxLon() - t * (a.getMaxLon() - a.getMinLon())),
FakeIdGenerator.makeFakeId());
case 3:
return new Node((float)(a.getMaxLat() - t * (a.getMaxLat()-a.getMinLat())),
a.getMinLon(), FakeIdGenerator.makeFakeId());
default:
throw new IllegalArgumentException("edge has invalid value");
}
}
@Override
public String toString() {
return "EdgeHit " + edge + "@" + t;
}
}
private static EdgeHit getEdgeHit(Bounds a, Node p)
{
// mkgmap uses ints for lat/lon where a digit is 1 / (2^24) of a degree
// (see Utils.toMapUnit()). So a tolerance of 10 is 0.000214576721191 degrees
// or about 0.72 arc seconds.
return getEdgeHit(a, p, 0.0002f);
}
private static EdgeHit getEdgeHit(Bounds a, Node p, float tolerance)
{
float lat = p.getLat();
float lon = p.getLon();
float minLat = a.getMinLat();
float maxLat = a.getMaxLat();
float minLong = a.getMinLon();
float maxLong = a.getMaxLon();
log.info(String.format("getEdgeHit: (%f %f) (%f %f %f %f)",
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 Node p.
*/
private static EdgeHit getNextEdgeHit(Bounds a, Node p)
{
float lat = p.getLat();
float lon = p.getLon();
float minLat = a.getMinLat();
float maxLat = a.getMaxLat();
float minLong = a.getMinLon();
float maxLong = a.getMaxLon();
log.info(String.format("getNextEdgeHit: (%f %f) (%f %f %f %f)",
lat, lon, minLat, minLong, maxLat, maxLong));
// shortest distance to border (init with distance to southern border)
float 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 static void concatenateWays(List<Way> ways, Bounds bounds, Tile tile) {
Map<Node, Way> beginMap = new HashMap<Node, Way>();
for (Way w : ways) {
if (!w.isClosed()) {
List<Node> points = w.getNodes();
beginMap.put(points.get(0), w);
}
}
int merged = 1;
while (merged > 0) {
merged = 0;
for (Way w1 : ways) {
if (w1.isClosed()) {
continue;
}
List<Node> points1 = w1.getNodes();
Way w2 = beginMap.get(points1.get(points1.size()-1));
if (w2 != null) {
log.info("merging: ", ways.size(), w1.getId(), w2.getId());
List<Node> points2 = w2.getNodes();
Way wm;
if (FakeIdGenerator.isFakeId(w1.getId())) {
wm = w1;
} else {
wm = new Way(FakeIdGenerator.makeFakeId());
ways.remove(w1);
ways.add(wm);
wm.getNodes().addAll(points1);
beginMap.put(points1.get(0), wm);
// only copy the name tags
for (String tag : w1.getTags()) {
if (tag.equals("name") || tag.endsWith(":name")) {
wm.setAttribute(tag, w1.getAttribute(tag));
}
}
}
wm.getNodes().addAll(points2);
ways.remove(w2);
beginMap.remove(points2.get(0));
merged++;
break;
}
}
}
// join up coastline segments whose end points are less than
// maxCoastlineGap meters apart
if(maxCoastlineGap > 0) {
boolean changed = true;
while(changed) {
changed = false;
for(Way w1 : ways) {
if(w1.isClosed()) {
continue;
}
List<Node> points1 = w1.getNodes();
Node w1e = points1.get(points1.size() - 1);
if(bounds.isOnBoundary(w1e)) {
continue;
}
Way nearest = null;
double smallestGap = Double.MAX_VALUE;
for(Way w2 : ways) {
if(w1 == w2 || w2.isClosed()) {
continue;
}
List<Node> points2 = w2.getNodes();
Node w2s = points2.get(0);
if(bounds.isOnBoundary(w2s)) {
continue;
}
double gap = MyMath.dist(w1e, w2s);
if(gap < smallestGap) {
nearest = w2;
smallestGap = gap;
}
}
if(nearest != null && smallestGap < maxCoastlineGap) {
Node w2s = nearest.getNodes().get(0);
log.warn("Bridging " + (int)smallestGap + "m gap in coastline from " +
w1e.toUrl() + " to " + w2s.toUrl());
Way wm;
if (FakeIdGenerator.isFakeId(w1.getId())) {
wm = w1;
} else {
wm = new Way(FakeIdGenerator.makeFakeId());
ways.remove(w1);
ways.add(wm);
wm.getNodes().addAll(points1);
wm.cloneTags(w1);
}
wm.getNodes().addAll(nearest.getNodes());
ways.remove(nearest);
// make a line that shows the filled gap
Way w = new Way(FakeIdGenerator.makeFakeId());
// TODO: So we need a style definition for this
w.setAttribute("natural", "coastline-gap");
w.addNode(w1e);
w.addNode(w2s);
tile.addWay(w);
changed = true;
break;
}
}
}
}
}
}