package org.osm2world.core.map_data.creation;
import static java.lang.Boolean.*;
import static java.lang.Double.NaN;
import static java.lang.Math.*;
import static java.util.Arrays.asList;
import static java.util.Collections.*;
import static org.osm2world.core.math.GeometryUtil.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup;
import org.openstreetmap.josm.plugins.graphview.core.data.Tag;
import org.openstreetmap.josm.plugins.graphview.core.data.TagGroup;
import org.osm2world.core.map_data.data.MapArea;
import org.osm2world.core.map_data.data.MapNode;
import org.osm2world.core.math.AxisAlignedBoundingBoxXZ;
import org.osm2world.core.math.InvalidGeometryException;
import org.osm2world.core.math.LineSegmentXZ;
import org.osm2world.core.math.PolygonWithHolesXZ;
import org.osm2world.core.math.SimplePolygonXZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.math.datastructures.IntersectionTestObject;
import org.osm2world.core.osm.data.OSMData;
import org.osm2world.core.osm.data.OSMElement;
import org.osm2world.core.osm.data.OSMMember;
import org.osm2world.core.osm.data.OSMNode;
import org.osm2world.core.osm.data.OSMRelation;
import org.osm2world.core.osm.data.OSMWay;
import org.osm2world.core.osm.ruleset.HardcodedRuleset;
import org.osm2world.core.osm.ruleset.Ruleset;
/**
* utility class for creating areas from multipolygon relations,
* including those with non-closed member ways.
*
* Known Limitations:<ul>
* <li>This cannot reliably handle touching inner rings consisting
* of non-closed ways.</li>
* <li>Closed touching rings will not break the calculation,
* but are represented as multiple touching holes.</li>
* </ul>
*/
final class MultipolygonAreaBuilder {
/** prevents instantiation */
private MultipolygonAreaBuilder() { }
/**
* Creates areas for a multipolygon relation.
* Also adds this area to the adjacent nodes using
* {@link MapNode#addAdjacentArea(MapArea)}.
*
* @param relation the multipolygon relation
* @param nodeMap map from {@link OSMNode}s to {@link MapNode}s
*
* @return constructed area(s), multiple areas will be created if there
* is more than one outer ring. Empty for invalid multipolygons.
*/
public static final Collection<MapArea> createAreasForMultipolygon(
OSMRelation relation, Map<OSMNode, MapNode> nodeMap) {
if (isSimpleMultipolygon(relation)) {
return createAreasForSimpleMultipolygon(relation, nodeMap);
} else {
return createAreasForAdvancedMultipolygon(relation, nodeMap);
}
}
private static final boolean isSimpleMultipolygon(OSMRelation relation) {
int numberOuters = 0;
boolean closedWays = true;
for (OSMMember member : relation.relationMembers) {
if ("outer".equals(member.role)
&& member.member instanceof OSMWay) {
numberOuters += 1;
}
if (("outer".equals(member.role) || "inner".equals(member.role))
&& member.member instanceof OSMWay) {
if (!((OSMWay)member.member).isClosed()) {
closedWays = false;
break;
}
}
}
return numberOuters == 1 && closedWays;
}
/**
* handles the common simple case with only one outer way.
* Expected to be faster than the more general method
* {@link #createAreasForAdvancedMultipolygon(OSMRelation, Map)}
*
* @param relation has to be a simple multipolygon relation
* @param nodeMap
*/
private static final Collection<MapArea> createAreasForSimpleMultipolygon(
OSMRelation relation, Map<OSMNode, MapNode> nodeMap) {
assert isSimpleMultipolygon(relation);
OSMElement tagSource = null;
List<MapNode> outerNodes = null;
List<List<MapNode>> holes = new ArrayList<List<MapNode>>();
for (OSMMember member : relation.relationMembers) {
if (member.member instanceof OSMWay) {
OSMWay way = (OSMWay)member.member;
if ("inner".equals(member.role)) {
List<MapNode> hole = new ArrayList<MapNode>(way.nodes.size());
for (OSMNode node : ((OSMWay)member.member).nodes) {
hole.add(nodeMap.get(node));
//TODO: add area as adjacent to node for inners' nodes, too?
}
holes.add(hole);
} else if ("outer".equals(member.role)) {
tagSource = relation.tags.size() > 1 ? relation : way;
outerNodes = new ArrayList<MapNode>(way.nodes.size());
for (OSMNode node : way.nodes) {
outerNodes.add(nodeMap.get(node));
}
}
}
}
return singleton(new MapArea(tagSource, outerNodes, holes));
}
private static final Collection<MapArea> createAreasForAdvancedMultipolygon(
OSMRelation relation, Map<OSMNode, MapNode> nodeMap) {
List<NodeSequence> innersAndOuters = new ArrayList<NodeSequence>();
/* collect ways */
for (OSMMember member : relation.relationMembers) {
if (member.member instanceof OSMWay
&& ("outer".equals(member.role)
|| "inner".equals(member.role)) ) {
innersAndOuters.add(new NodeSequence(
(OSMWay)member.member, nodeMap));
}
}
/* build rings, then polygons from the ways */
List<Ring> rings = buildRings(innersAndOuters);
if (rings != null) {
return buildPolygonsFromRings(relation, rings);
} else {
return Collections.emptyList();
}
}
/**
* builds closed rings from any mixture of closed and unclosed segments
*
* @return null if building closed rings isn't possible
*/
private static final List<Ring> buildRings(
List<NodeSequence> sequences) {
List<Ring> closedRings = new ArrayList<Ring>();
NodeSequence currentRing = null;
while (sequences.size() > 0) {
if (currentRing == null) {
// start a new ring with any remaining node sequence
currentRing = sequences.remove(sequences.size() - 1);
} else {
// try to continue the ring by appending a node sequence
NodeSequence assignedSequence = null;
for (NodeSequence sequence : sequences) {
if (currentRing.tryAdd(sequence)) {
assignedSequence = sequence;
break;
}
}
if (assignedSequence != null) {
sequences.remove(assignedSequence);
} else {
return null;
}
}
// check whether the ring under construction is closed
if (currentRing != null && currentRing.isClosed()) {
try {
closedRings.add(new Ring(currentRing));
currentRing = null;
} catch (InvalidGeometryException e) {
throw new InvalidGeometryException(String.format(
"self-intersecting ring (with %d nodes)",
currentRing.size() - 1), e);
}
}
}
if (currentRing != null) {
// the last ring could not be closed
return null;
}
return closedRings;
}
/**
* @param rings rings to build polygons from; will be empty afterwards
*/
private static final Collection<MapArea> buildPolygonsFromRings(
OSMRelation relation, List<Ring> rings) {
Collection<MapArea> finishedPolygons =
new ArrayList<MapArea>(rings.size() / 2);
/* build polygon */
while (!rings.isEmpty()) {
/* find an outer ring */
Ring outerRing = null;
for (Ring candidate : rings) {
boolean containedInOtherRings = false;
for (Ring otherRing : rings) {
if (otherRing != candidate
&& otherRing.containsRing(candidate)) {
containedInOtherRings = true;
break;
}
}
if (!containedInOtherRings) {
outerRing = candidate;
break;
}
}
/* find inner rings of that ring */
Collection<Ring> innerRings = new ArrayList<Ring>();
for (Ring ring : rings) {
if (ring != outerRing && outerRing.containsRing(ring)) {
boolean containedInOtherRings = false;
for (Ring otherRing : rings) {
if (otherRing != ring && otherRing != outerRing
&& otherRing.containsRing(ring)) {
containedInOtherRings = true;
break;
}
}
if (!containedInOtherRings) {
innerRings.add(ring);
}
}
}
/* create a new area and remove the used rings */
List<List<MapNode>> holes = new ArrayList<List<MapNode>>(innerRings.size());
List<SimplePolygonXZ> holesXZ = new ArrayList<SimplePolygonXZ>(innerRings.size());
for (Ring innerRing : innerRings) {
holes.add(innerRing.closedNodeSequence);
holesXZ.add(innerRing.getPolygon());
}
MapArea area = new MapArea(relation, outerRing.getNodeLoop(), holes,
new PolygonWithHolesXZ(outerRing.getPolygon(), holesXZ));
finishedPolygons.add(area);
rings.remove(outerRing);
rings.removeAll(innerRings);
}
return finishedPolygons;
}
private static final TagGroup COASTLINE_NODE_TAGS = new MapBasedTagGroup(
new Tag("osm2world:note", "fake node from coastline processing"));
/**
* turns all coastline ways into {@link MapArea}s
* based on an artificial natural=water multipolygon relation.
*
* It relies on the direction-dependent drawing of coastlines.
* If coastlines are incomplete, then it is attempted to connect them
* to proper rings. One assumption being used is that they are complete
* within the file's bounds.
*
* It cannot distinguish between water and land tiles if there is no
* coastline at all (it will then guess based on the tags being used),
* but should be able to handle all other cases.
*/
public static final Collection<MapArea> createAreasForCoastlines(
OSMData osmData, Map<OSMNode, MapNode> nodeMap,
Collection<MapNode> mapNodes, AxisAlignedBoundingBoxXZ fileBoundary) {
long highestRelationId = 0;
long highestNodeId = 0;
List<OSMWay> coastlineWays = new ArrayList<OSMWay>();
for (OSMWay way : osmData.getWays()) {
if (way.tags.contains("natural", "coastline")) {
coastlineWays.add(way);
}
}
for (OSMRelation relation : osmData.getRelations()) {
if (relation.id > highestRelationId) {
highestRelationId = relation.id;
}
}
for (OSMNode node : osmData.getNodes()) {
if (node.id > highestNodeId) {
highestNodeId = node.id;
}
}
if (fileBoundary != null) {
/* build node sequences (may be closed or unclosed) */
List<NodeSequence> origCoastlines = new ArrayList<NodeSequence>();
for (OSMWay coastlineWay : coastlineWays) {
origCoastlines.add(new NodeSequence(coastlineWay, nodeMap));
}
/* find coastline intersections with bounding box.
* They will be inserted into the rings that intersect the coastline,
* and into a list (sorted counterclockwise) of intersection nodes.
*/
List<NodeOnBBox> bBoxNodes = new ArrayList<NodeOnBBox>();
for (final LineSegmentXZ side : getSidesClockwise(fileBoundary)) {
List<NodeOnBBox> intersectionsSide =
new ArrayList<NodeOnBBox>();
for (NodeSequence coastline : origCoastlines) {
for (int i = 0; i + 1 < coastline.size(); i++) {
VectorXZ r1 = coastline.get(i).getPos();
VectorXZ r2 = coastline.get(i + 1).getPos();
VectorXZ intersection = getLineSegmentIntersection(
side.p1, side.p2, r1, r2);
if (intersection != null) {
MapNode intersectionNode;
if (intersection.equals(r1)) {
intersectionNode = coastline.get(i);
} else if (intersection.equals(r2)) {
intersectionNode = coastline.get(i + 1);
} else {
intersectionNode = createFakeMapNode(intersection,
++highestNodeId, osmData, nodeMap, mapNodes);
coastline.add(i + 1, intersectionNode);
i += 1;
}
intersectionsSide.add(new NodeOnBBox(intersectionNode,
isRightOf(r1, side.p1, side.p2)));
}
}
}
/* add intersections for this side of the bbox,
* sorted by distance from corner */
Collections.sort(intersectionsSide, new Comparator<NodeOnBBox>() {
@Override public int compare(NodeOnBBox n1, NodeOnBBox n2) {
return Double.compare(
n1.node.getPos().distanceTo(side.p1),
n2.node.getPos().distanceTo(side.p1));
}
});
bBoxNodes.addAll(intersectionsSide);
MapNode cornerNode = createFakeMapNode(side.p2,
++highestNodeId, osmData, nodeMap, mapNodes);
bBoxNodes.add(new NodeOnBBox(cornerNode, null));
}
/* rings are possibly shortened or split by removing all nodes
* outside the bbox. */
List<NodeSequence> modifiedCoastlines = new ArrayList<NodeSequence>();
for (NodeSequence origCoastline : origCoastlines) {
NodeSequence modifiedCoastline = new NodeSequence();
for (MapNode node : origCoastline) {
boolean isOnBBox = false;
for (NodeOnBBox bBoxNode : bBoxNodes) {
if (bBoxNode.node.equals(node)) {
isOnBBox = true;
}
}
if (fileBoundary.contains(node) || isOnBBox) {
modifiedCoastline.add(node);
} else {
if (!modifiedCoastline.isEmpty()) {
modifiedCoastlines.add(modifiedCoastline);
modifiedCoastline = new NodeSequence();
}
}
}
if (!modifiedCoastline.isEmpty()) {
modifiedCoastlines.add(modifiedCoastline);
}
}
/* parts of the bounding box between outgoing and incoming
* intersection nodes are used as additional coastline sections */
List<NodeSequence> bboxSections = new ArrayList<NodeSequence>();
if (bBoxNodes.size() > 4) { //more than just corners
int firstIntersectionIndex = -1;
int currentIndex = 0;
List<MapNode> currentSequence = null;
while (currentIndex != firstIntersectionIndex) {
NodeOnBBox currentBBoxNode = bBoxNodes.get(currentIndex);
if (currentBBoxNode.outgoingIntersection == TRUE) {
currentSequence = new ArrayList<MapNode>();
currentSequence.add(currentBBoxNode.node);
if (firstIntersectionIndex == -1) {
firstIntersectionIndex = currentIndex;
}
} else if (currentBBoxNode.outgoingIntersection == FALSE) {
if (currentSequence != null) {
currentSequence.add(currentBBoxNode.node);
NodeSequence finishedBboxPart = new NodeSequence();
finishedBboxPart.addAll(currentSequence);
bboxSections.add(finishedBboxPart);
currentSequence = null;
}
} else {
if (currentSequence != null) {
currentSequence.add(currentBBoxNode.node);
}
}
currentIndex = (currentIndex + 1) % bBoxNodes.size();
}
}
/* construct closed rings and turn them into polygons with holes
* (as if the coastlines were multipolygon member ways) */
List<Ring> closedRings;
if (!bboxSections.isEmpty()) {
modifiedCoastlines.addAll(bboxSections);
closedRings = buildRings(modifiedCoastlines);
} else {
closedRings = buildRings(modifiedCoastlines);
if (closedRings != null) {
/* if there is an island, but no coastline intersects
* the boundary, create a boundary around the entire tile.
* Do the same for water tiles (tiles without any land). */
boolean hasIsland = false;
for (Ring closedRing : closedRings) {
if (!closedRing.getPolygon().isClockwise()) {
hasIsland = true;
break;
}
}
if (hasIsland || isProbablySeaTile(osmData)) {
NodeSequence boundaryRing = new NodeSequence();
for (VectorXZ pos : fileBoundary.polygonXZ().getVertices()) {
boundaryRing.add(createFakeMapNode(pos,
++highestNodeId, osmData, nodeMap, mapNodes));
}
boundaryRing.add(boundaryRing.get(0));
closedRings.add(new Ring(boundaryRing));
}
}
}
if (closedRings != null) {
OSMRelation relation = new OSMRelation(new MapBasedTagGroup(
new Tag("type", "multipolygon"), new Tag("natural", "water")),
highestRelationId + 1, 0);
return buildPolygonsFromRings(relation, closedRings);
}
}
return emptyList();
}
private static final List<LineSegmentXZ> getSidesClockwise(
AxisAlignedBoundingBoxXZ fileBoundary) {
return asList(
new LineSegmentXZ(fileBoundary.topLeft(), fileBoundary.topRight()),
new LineSegmentXZ(fileBoundary.topRight(), fileBoundary.bottomRight()),
new LineSegmentXZ(fileBoundary.bottomRight(), fileBoundary.bottomLeft()),
new LineSegmentXZ(fileBoundary.bottomLeft(), fileBoundary.topLeft()));
}
private static MapNode createFakeMapNode(VectorXZ pos, long nodeId,
OSMData osmData, Map<OSMNode, MapNode> nodeMap,
Collection<MapNode> mapNodes) {
OSMNode osmNode = new OSMNode(NaN, NaN,
COASTLINE_NODE_TAGS, nodeId + 1);
osmData.getNodes().add(osmNode);
MapNode mapNode = new MapNode(pos, osmNode);
mapNodes.add(mapNode);
nodeMap.put(osmNode, mapNode);
return mapNode;
}
/**
* guesses whether this is a pure sea tile (no land at all)
*/
private static boolean isProbablySeaTile(OSMData osmData) {
boolean anySeaTag = false;
Ruleset ruleset = new HardcodedRuleset();
@SuppressWarnings("unchecked")
List<Collection<? extends OSMElement>> collections = asList(
osmData.getWays(), osmData.getNodes());
for (Collection<? extends OSMElement> collection : collections) {
for (OSMElement element : collection) {
for (Tag tag : element.tags) {
if (ruleset.isLandTag(tag)) return false;
anySeaTag |= ruleset.isSeaTag(tag);
}
}
}
return anySeaTag;
}
private static final class NodeSequence extends ArrayList<MapNode> {
private static final long serialVersionUID = -1189277554247756781L; //generated SerialUID
/**
* creates an empty sequence
*/
public NodeSequence() {
super();
}
/**
* creates a node sequence from an {@link OSMWay}
*/
public NodeSequence(OSMWay way, Map<OSMNode, MapNode> nodeMap) {
super(way.nodes.size());
for (OSMNode wayNode : way.nodes) {
add(nodeMap.get(wayNode));
}
}
/**
* tries to add another sequence onto the start or end of this one.
* If it succeeds, the other sequence may also be modified and
* should be considered "spent".
*/
public boolean tryAdd(NodeSequence other) {
if (getLastNode() == other.getFirstNode()) {
//add the sequence at the end
remove(size() - 1);
addAll(other);
return true;
} else if (getLastNode() == other.getLastNode()) {
//add the sequence backwards at the end
remove(size() - 1);
Collections.reverse(other);
addAll(other);
return true;
} else if (getFirstNode() == other.getLastNode()) {
//add the sequence at the beginning
remove(0);
addAll(0, other);
return true;
} else if (getFirstNode() == other.getFirstNode()) {
//add the sequence backwards at the beginning
remove(0);
Collections.reverse(other);
addAll(0, other);
return true;
} else {
return false;
}
}
private MapNode getFirstNode() {
return get(0);
}
private MapNode getLastNode() {
return get(size() - 1);
}
public boolean isClosed() {
return getFirstNode() == getLastNode();
}
}
private static final class Ring implements IntersectionTestObject {
private final NodeSequence closedNodeSequence;
private final SimplePolygonXZ polygon;
public Ring(NodeSequence closedNodeSequence) {
assert closedNodeSequence.isClosed();
this.closedNodeSequence = closedNodeSequence;
polygon = MapArea.polygonFromMapNodeLoop(closedNodeSequence);
}
@Override
public AxisAlignedBoundingBoxXZ getAxisAlignedBoundingBoxXZ() {
double minX = Double.POSITIVE_INFINITY, minZ = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY, maxZ = Double.NEGATIVE_INFINITY;
for (MapNode n : closedNodeSequence) {
minX = min(minX, n.getPos().x); minZ = min(minZ, n.getPos().z);
maxX = max(maxX, n.getPos().x); maxZ = max(maxZ, n.getPos().z);
}
return new AxisAlignedBoundingBoxXZ(minX, minZ, maxX, maxZ);
}
private List<MapNode> getNodeLoop() {
return closedNodeSequence;
}
public SimplePolygonXZ getPolygon() {
return polygon;
}
public boolean containsRing(Ring other) {
return this.getPolygon().contains(other.getPolygon());
}
}
private static class NodeOnBBox {
/** true for outgoing, false for incoming, null for other bbox nodes */
public final Boolean outgoingIntersection;
public final MapNode node;
private NodeOnBBox(MapNode node, Boolean outgoingIntersection) {
this.node = node;
this.outgoingIntersection = outgoingIntersection;
}
@Override
public String toString() {
return "(" + outgoingIntersection + ", " + node.getOsmNode().id +
"@" + node.getPos() + ")";
}
}
}