/**
* 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) 2011 sk750
*/
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.mkgmap.reader.osm.FakeIdGenerator;
import net.sharenav.osmToShareNav.Configuration;
import net.sharenav.osmToShareNav.MyMath;
import net.sharenav.osmToShareNav.OsmParser;
import net.sharenav.osmToShareNav.model.Bounds;
import net.sharenav.osmToShareNav.model.Member;
import net.sharenav.osmToShareNav.model.Node;
import net.sharenav.osmToShareNav.model.Relation;
import net.sharenav.osmToShareNav.model.Way;
public class SeaGenerator2 {
public float minLat = Float.MAX_VALUE;
public float minLon = Float.MAX_VALUE;
public float maxLat = -Float.MAX_VALUE;
public float maxLon = -Float.MAX_VALUE;
public float minMapLat = Float.MAX_VALUE;
public float minMapLon = Float.MAX_VALUE;
public float maxMapLat = -Float.MAX_VALUE;
public float maxMapLon = -Float.MAX_VALUE;
// for debugging
private boolean onlyOutlines = false;
// this helps some areas like Canary islands
// but may be sub-optimal, may cause smaller triangles
// than necessary; would be better
// if tile splitting code and triangle splitting code
// would take care of things
private boolean interimNodes = true;
private static boolean generateSea = true;
private static boolean generateSeaUsingMP = false;
private static boolean allowSeaSectors = false;
private static boolean extendSeaSectors = true;
private static int maxCoastlineGap = 0;
private static Configuration configuration;
private boolean foundCoast = false;
private static boolean debugSea = false;
Bounds seaBounds;
Bounds mapBounds;
/**
* 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;
}
public void generateSea(OsmParser parser) {
seaBounds = new Bounds();
// remember coastlines and add them to landways
String natural;
ArrayList<Way> landWays = new ArrayList<Way>();
for (Way w: parser.getWays()) {
natural = w.getAttribute("natural");
if (natural != null) {
if ("coastline".equals(natural)) {
if (debugSea) {
//System.out.println("Create land from coastline " + w.toUrl());
}
// for closed ways, save memory and do not create new ways
if (w.isClosed()) {
landWays.add(w);
} else {
long landId = FakeIdGenerator.makeFakeId();
Way wLand = new Way(landId, w);
landWays.add(wLand);
}
// find minimum/maximum lat and lon for the bundle
seaBounds.extend(w.getBounds());
}
}
}
Node sw = new Node(seaBounds.minLat, seaBounds.minLon, FakeIdGenerator.makeFakeId());
Node se = new Node(seaBounds.minLat, seaBounds.maxLon, FakeIdGenerator.makeFakeId());
Node nw = new Node(seaBounds.maxLat, seaBounds.minLon, FakeIdGenerator.makeFakeId());
Node ne = new Node(seaBounds.maxLat, seaBounds.maxLon, FakeIdGenerator.makeFakeId());
// use sea tiles: divide map area into separate parts for more efficient processing (experimental)
if (configuration.getUseSeaTiles()) {
// divide map area into parts; run sea multipolygon generation for each part
final int latDivider = (int) MyMath.dist(sw, nw) / 5000 + 2;
final int lonDivider = (int) MyMath.dist(sw, se) / 5000 + 2;
Bounds allMapBounds = seaBounds.clone();
for (int lat = 0; lat < latDivider ; lat++) {
for (int lon = 0; lon < lonDivider ; lon++) {
// loop x & y
sw = new Node(allMapBounds.minLat + (allMapBounds.maxLat - allMapBounds.minLat) / latDivider * lat,
allMapBounds.minLon + (allMapBounds.maxLon - allMapBounds.minLon) / lonDivider * lon,
FakeIdGenerator.makeFakeId());
nw = new Node(allMapBounds.minLat + (allMapBounds.maxLat - allMapBounds.minLat) / latDivider * (lat + 1),
allMapBounds.minLon + (allMapBounds.maxLon - allMapBounds.minLon) / lonDivider * lon,
FakeIdGenerator.makeFakeId());
se = new Node(allMapBounds.minLat + (allMapBounds.maxLat - allMapBounds.minLat) / latDivider * lat,
allMapBounds.minLon + (allMapBounds.maxLon - allMapBounds.minLon) / lonDivider * (lon + 1),
FakeIdGenerator.makeFakeId());
ne = new Node(allMapBounds.minLat + (allMapBounds.maxLat - allMapBounds.minLat) / latDivider * (lat + 1),
allMapBounds.minLon + (allMapBounds.maxLon - allMapBounds.minLon) / lonDivider * (lon + 1),
FakeIdGenerator.makeFakeId());
seaBounds.minLat = sw.lat;
seaBounds.maxLat = nw.lat;
seaBounds.minLon = sw.lon;
seaBounds.maxLon = se.lon;
// whole map area sea generation
float seaMargin = 0.000005f;
float seaTileMargin = 0.000010f;
seaBounds.minLat -= seaMargin;
seaBounds.minLon -= seaMargin;
seaBounds.maxLat += seaMargin;
seaBounds.maxLon += seaMargin;
mapBounds = seaBounds.clone();
mapBounds.minLat += seaTileMargin;
mapBounds.minLon += seaTileMargin;
mapBounds.maxLat -= seaTileMargin;
mapBounds.maxLon -= seaTileMargin;
if (configuration.verbose >= 0) {
if (debugSea) {
System.out.println("seaBounds: " + seaBounds);
}
if (debugSea) {
System.out.println("mapBounds: " + mapBounds);
}
}
landWays.clear();
foundCoast = false;
for (Way w: parser.getWays()) {
natural = w.getAttribute("natural");
if (natural != null) {
if ("coastline".equals(natural)) {
//System.out.println("Create land from coastline " + w.toUrl());
// for closed ways, save memory and do not create new ways
// check if in map bounds; if not, skip this
// FIXME should cut ways on sea tile boundary
Way boundedWay = new Way(w.id);
for (Node node : w.getNodes()) {
if (mapBounds.isIn(node.getLat(), node.getLon())) {
boundedWay.addNodeIfNotEqualToFirstNodeOfTwo(node);
} else {
// way is going out of sea tile, cut here, add the
// first part
if (boundedWay.getNodeCount() != 0) {
long readyId = FakeIdGenerator.makeFakeId();
Way wReady = new Way(readyId, boundedWay);
landWays.add(wReady);
foundCoast = true;
if (debugSea) {
System.out.println("Node out of bound, splitting way");
System.out.println("w: " + w +
" wReady: " + wReady +
" boundedWay: " + boundedWay);
}
}
boundedWay = new Way(w.id);
}
}
if (boundedWay.isValid()) {
long landId = FakeIdGenerator.makeFakeId();
Way wLand = new Way(landId, boundedWay);
landWays.add(wLand);
foundCoast = true;
}
}
}
}
generateSeaMultiPolygon(parser, sw, se, nw, ne, landWays, mapBounds);
}
}
} else {
// whole map area sea generation
mapBounds = seaBounds.clone();
float seaMargin = 0.000005f;
seaBounds.minLat -= seaMargin;
seaBounds.minLon -= seaMargin;
seaBounds.maxLat += seaMargin;
seaBounds.maxLon += seaMargin;
if (debugSea) {
System.out.println("seaBounds: " + seaBounds);
System.out.println("mapBounds: " + mapBounds);
}
generateSeaMultiPolygon(parser, sw, se, nw, ne, landWays, mapBounds);
}
}
public void generateSeaMultiPolygon(OsmParser parser, Node sw, Node se, Node nw, Node ne, ArrayList<Way> landWays, Bounds mapBounds) {
long seaId = FakeIdGenerator.makeFakeId();
Way sea = new Way(seaId);
sea.addNode(sw);
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
sea.addNodeIfNotEqualToLastNodeWithInterimNodes(nw);
sea.addNodeIfNotEqualToLastNodeWithInterimNodes(ne);
sea.addNodeIfNotEqualToLastNodeWithInterimNodes(se);
sea.addNodeIfNotEqualToLastNodeWithInterimNodes(sw);
} else {
sea.addNodeIfNotEqualToLastNode(nw);
sea.addNodeIfNotEqualToLastNode(ne);
sea.addNodeIfNotEqualToLastNode(se);
sea.addNodeIfNotEqualToLastNode(sw);
}
long multiId = FakeIdGenerator.makeFakeId();
Relation seaRelation = new Relation(multiId);
if (!onlyOutlines) {
seaRelation.setAttribute("type", "multipolygon");
seaRelation.setAttribute("natural", "sea");
}
Member mInner;
// handle islands (closed shoreline components) first (they're easy)
// add closed landways (islands, islets) to parser and to sea relation
// these are inner members in the sea relation,
// in other words holes in the sea
Iterator<Way> it = landWays.iterator();
while (it.hasNext()) {
Way w = it.next();
// check if in map bounds; if not, skip this
// FIXME should cut ways on sea tile boundary
Way boundedWay = new Way(w.id);
for (Node node : w.getNodes()) {
if (mapBounds.isIn(node.getLat(), node.getLon())) {
boundedWay.addNode(node);
} else {
// Nodes for this way are missing, problem in OSM or simply
// out of bounding box.
// Three different cases are possible:
// missing at the start, in the middle or at the end.
// We simply add the current way and start a new one
// with shared attributes.
// Degenerate ways are not added, so don't care about
// this here.
//if (boundedWay.getNodeCount() != 0) {
// Way tmp_way = new Way(boundedWay);
// parser.addWay(boundedWay);
// way = tmp_way;
//}
}
}
if (boundedWay.isValid() && boundedWay.getNodeCount() != 0) {
foundCoast = true;
} else {
it.remove();
continue;
}
if (boundedWay.isClosed()) {
//System.out.println("adding island " + w);
parser.addWay(boundedWay);
it.remove();
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
boundedWay.setAttribute("natural", "seaoutline");
}
mInner = new Member("way", boundedWay.id, "inner");
seaRelation.add(mInner);
}
}
// while relation handling code does concatenate ways, we need
// this to see where the start and end for coastlines are
// so we can decide how to connect partial coastlines to e.g. map borders
concatenateWays(landWays, mapBounds, parser, seaRelation, onlyOutlines);
// there may be more islands now
it = landWays.iterator();
while (it.hasNext()) {
Way w = it.next();
if (w.isClosed()) {
if (debugSea) {
System.out.println("after concatenation: adding island " + w);
}
parser.addWay(w);
it.remove();
mInner = new Member("way", w.id, "inner");
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
w.setAttribute("natural", "seaoutline");
}
seaRelation.add(mInner);
}
}
// handle non-closed coastlines
// * those which intersect the map boundary
// * possible map bugs
SortedMap<EdgeHit, Way> hitMap = new TreeMap<EdgeHit, Way>();
Way seaSector = null;
for (Way w : landWays) {
List<Node> points = w.getNodes();
Node pStart = points.get(0);
Node pEnd = points.get(points.size()-1);
EdgeHit hStart = getEdgeHit(mapBounds, pStart);
EdgeHit hEnd = getEdgeHit(mapBounds, pEnd);
// FIXME don't force this after coastlines are truly cut at map/seatile border
hStart = null;
hEnd = null;
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());
if (debugSea) {
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;
}
if (debugSea) {
System.out.println("dist from coastline start to end: " + MyMath.dist(pStart, pEnd));
}
boolean nearlyClosed = (MyMath.dist(pStart, pEnd) < 0.1 * length);
// FIXME enable again when coastlines are cut exactly at tile borders so this doesn't cause trouble
if (false && nearlyClosed) {
if (debugSea) {
System.out.println("handling nearlyClosed coastline: " + w);
}
// close the way
points.add(pStart);
if (generateSeaUsingMP) {
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
w.setAttribute("natural", "seaoutline");
}
mInner = new Member("way", w.id, "inner");
seaRelation.add(mInner);
// polish.api.bigstyles
short t = w.getType(configuration);
parser.addWay(w);
} else {
System.out.println("ERROR: SeaGenerator: can only create sea properly as relations");
}
}
else if (allowSeaSectors && false) { // this part appears to cause trouble, removed
if (debugSea) {
System.out.println("handling allowSeaSectors coastline: " + w);
}
seaId = FakeIdGenerator.makeFakeId();
seaSector = new Way(seaId);
EdgeHit startEdgeHit = getNextEdgeHit(mapBounds, pStart);
EdgeHit endEdgeHit = getNextEdgeHit(mapBounds, pEnd);
int startedge = startEdgeHit.edge;
int endedge = endEdgeHit.edge;
if (debugSea) {
System.out.println("startedge: " + startedge + " endedge: " + endedge);
}
if (false || false) {
Node p;
//System.out.println("edge: " + edge + " val: " + val);
if (endedge < startedge) {
endedge += 4;
}
for (int i=endedge; i > startedge; i--) {
int edge = i % 4;
float val = 0.0f;
if (debugSea) {
System.out.println("edge: " + edge + " val: " + val);
}
EdgeHit corner = new EdgeHit(edge, val);
p = corner.getPoint(mapBounds);
//log.debug("way: ", corner, p);
if (debugSea) {
System.out.println("way: corner: " + corner + " p: " + p);
}
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
seaSector.addNodeIfNotEqualToLastNodeWithInterimNodes(p);
} else {
seaSector.addNodeIfNotEqualToLastNode(p);
}
}
}
if (generateSeaUsingMP) {
parser.addWay(seaSector);
mInner = new Member("way", seaSector.id, "inner");
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
seaSector.setAttribute("natural", "seaoutline");
}
seaRelation.add(mInner);
//System.out.println("Added inner to sea relation: " + seaRelation.toString());
}
}
else if (extendSeaSectors) {
// create additional points at next border to prevent triangles from point 2
if (debugSea) {
System.out.println("Extend sea sector, way id: " + w.id);
}
if (null == hStart) {
// attach start of way to edge, with interim nodes
// when necessary
hStart = getNextEdgeHit(mapBounds, pStart);
// without interim nodes
//w.getNodes().add(0, hStart.getPoint(mapBounds));
// with interim nodes
Node p = hStart.getPoint(mapBounds);
Way helperWay = new Way(FakeIdGenerator.makeFakeId());
List<Node> oldpoints = w.getNodes();
helperWay.addNode(p);
if (debugSea) {
System.out.println("building the helper way");
}
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
helperWay.addNodeIfNotEqualToLastNodeWithInterimNodes(oldpoints.get(0));
} else {
helperWay.addNodeIfNotEqualToLastNode(oldpoints.get(0));
}
List<Node> helperPoints = helperWay.getNodes();
for (int i = helperPoints.size()-1 ; i >= 0; i--) {
w.getNodes().add(0, helperPoints.get(i));
}
if (debugSea) {
System.out.println("startedge: " + hStart.edge);
}
}
if (null == hEnd) {
hEnd = getNextEdgeHit(mapBounds, pEnd);
Node p = hEnd.getPoint(mapBounds);
//w.getNodes().add(hEnd.getPoint(mapBounds));
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
w.addNodeIfNotEqualToLastNodeWithInterimNodes(p);
} else {
w.addNodeIfNotEqualToLastNode(p);
}
if (debugSea) {
System.out.println("endedge: " + hEnd.edge);
}
}
//log.debug("hits (second try): ", hStart, hEnd);
mInner = new Member("way", w.id, "inner");
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
w.setAttribute("natural", "seaoutline");
}
seaRelation.add(mInner);
hitMap.put(hStart, w);
hitMap.put(hEnd, null);
parser.addWay(w);
}
} else {
//log.debug("hits: ", hStart, hEnd);
hitMap.put(hStart, w);
hitMap.put(hEnd, null);
}
}
// now construct the outer sea polygon from the edge hits and
// map boundaries
NavigableSet<EdgeHit> hits = (NavigableSet<EdgeHit>) hitMap.keySet();
boolean shorelineReachesBoundary = false;
while (!hits.isEmpty()) {
long id = FakeIdGenerator.makeFakeId();
Way w = new Way(id);
//parser.addWay(w);
EdgeHit hit = hits.first();
EdgeHit hFirst = hit;
do {
Way segment = hitMap.get(hit);
if (debugSea) {
System.out.println("current hit: " + hit);
}
EdgeHit hNext;
if (segment != null) {
// could do better with adding segments to
// relation
// add the segment and get the "ending hit"
if (debugSea) {
System.out.println("adding sgement: " + segment);
}
for(Node p : segment.getNodes()) {
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
w.addNodeIfNotEqualToLastNodeWithInterimNodes(p);
} else {
w.addNodeIfNotEqualToLastNode(p);
}
}
hNext = getEdgeHit(mapBounds, segment.getNodes().get(segment.getNodes().size()-1));
} else { // segment == null
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
w.addNodeIfNotEqualToLastNodeWithInterimNodes(hit.getPoint(mapBounds));
} else {
w.addNodeIfNotEqualToLastNode(hit.getPoint(mapBounds));
}
hNext = hits.higher(hit);
if (hNext == null) {
hNext = hFirst;
}
Node p;
if (hit.compareTo(hNext) < 0) {
//log.info("joining: ", hit, hNext);
if (debugSea) {
System.out.println("joining compareTo < 0, hit: " + hit + " hNext: " + hNext);
}
for (int i=hit.edge; i<hNext.edge; i++) {
EdgeHit corner = new EdgeHit(i, 1.0);
p = corner.getPoint(mapBounds);
//log.debug("way: ", corner, p);
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
w.addNodeIfNotEqualToLastNodeWithInterimNodes(p);
} else {
w.addNodeIfNotEqualToLastNode(p);
}
}
}
else if (hit.compareTo(hNext) > 0) {
if (debugSea) {
System.out.println("joining compareTo > 0: " + hit + " hNext: " + hNext);
}
int hNextEdge = hNext.edge;
if (hit.edge >= hNext.edge) {
hNextEdge += 4;
}
for (int i=hit.edge; i < hNextEdge; i++) {
EdgeHit corner = new EdgeHit(i % 4, 1.0);
p = corner.getPoint(mapBounds);
//log.debug("way: ", corner, p);
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
w.addNodeIfNotEqualToLastNodeWithInterimNodes(p);
} else {
w.addNodeIfNotEqualToLastNode(p);
}
}
}
if (onlyOutlines || interimNodes || configuration.getDrawSeaOutlines()) {
w.addNodeIfNotEqualToLastNodeWithInterimNodes(hNext.getPoint(mapBounds));
} else {
w.addNodeIfNotEqualToLastNode(hNext.getPoint(mapBounds));
}
}
hits.remove(hit);
hit = hNext;
} while (!hits.isEmpty() && !hit.equals(hFirst));
if (!w.isClosed()) {
w.getNodes().add(w.getNodes().get(0));
}
parser.addWay(w);
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
w.setAttribute("natural", "seaoutline");
}
mInner = new Member("way", w.id, "inner");
seaRelation.add(mInner);
//System.out.println("Added inner member to sea relation: " + seaRelation.toString());
//System.out.println("adding non-island landmass, hits.size()=" + hits.size());
//islands.add(w);
shorelineReachesBoundary = true;
// FIXME instead of adding inner members to define land here,
// we could track the outline and add outer members to define sea.
// Depending on the map area, the resulting multipolygons could be lighter
// to handle in triangulation and the tile & way splitting.
// In country-level cases where a country has coast at only one border
// the savings could be significant.
// Some issues with this
// * sea might not be in only one closed way, but multiple ways
// ** relation code currently copes with one outer member, not mutiple
// ** trianglulation code probably needs to know which inner polygons
// belong to which outer polygon, so code would be needed for that
// * others?
// I think the more typical case however is that sea is in one piece.
// We can probably check if sea is in one or more pieces, and decide
// which kind of multipolygon to produce.
}
// add sea relation
if (foundCoast) {
if (generateSeaUsingMP) {
// create a multipolygon relation containing water as outer role
mInner = new Member("way", sea.id, "outer");
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
sea.setAttribute("natural", "seaoutline");
}
seaRelation.add(mInner);
parser.addWay(sea);
parser.addRelation(seaRelation);
//System.out.println("Adding sea relation: " + seaRelation.toString());
} else {
System.out.println("ERROR: SeaGenerator: can only create sea properly as relations");
}
} else {
// FIXME sometimes it's sea, deduce from contents and/or neighbouring tiles
if (debugSea) {
System.out.println("SeaGenerator: didn't find any coastline ways, assuming this seatile is land");
}
}
if (debugSea) {
System.out.println(seaRelation.toString());
}
}
/**
* 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) {
//System.out.println("getPoint: " + 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.
// this might need adjustment - was 0.0004
//return getEdgeHit(a, p, 0.04f);
return getEdgeHit(a, p, 0.0004f);
}
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();
//System.out.println(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();
if (debugSea) {
System.out.println(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,
OsmParser parser, Relation seaRelation, boolean onlyOutlines) {
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
// this could cause trouble near the edges
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;
}
}
// FIXME this causes trouble with the non-border-ending cut coastlines, disable for now
if (false && nearest != null && smallestGap < maxCoastlineGap) {
Node w2s = nearest.getNodes().get(0);
if (debugSea) {
System.out.println("SeaGenerator: 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);
}
// FIXME check if this is correct
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");
// no need for this probably, as we're bridging closeby nodes
//if (onlyOutlines) {
// w.addNodeIfNotEqualToLastNodeWithInterimNodes(w1e);
// w.addNodeIfNotEqualToLastNodeWithInterimNodes(w2s);
//} else {
w.addNode(w1e);
w.addNode(w2s);
//}
parser.addWay(w);
if (onlyOutlines || configuration.getDrawSeaOutlines()) {
w.setAttribute("natural", "seaoutline");
}
Member mInner = new Member("way", w.id, "inner");
seaRelation.add(mInner);
changed = true;
break;
}
}
}
}
}
}