package org.osm2world.core.math.algorithms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.osm2world.core.math.GeometryUtil;
import org.osm2world.core.math.InvalidGeometryException;
import org.osm2world.core.math.LineSegmentXZ;
import org.osm2world.core.math.SimplePolygonXZ;
import org.osm2world.core.math.TriangleXZ;
import org.osm2world.core.math.VectorXZ;
public final class EarClippingTriangulationUtil {
private EarClippingTriangulationUtil() { }
/**
* triangulates a two-dimensional polygon
* by creating a simple polygon first
* (integrating holes into the polygon outline),
* then using Ear Clipping on that simple polygon.
*/
public static final List<TriangleXZ> triangulate(
SimplePolygonXZ polygon,
Collection<SimplePolygonXZ> holes) {
/*
* store the outline of the polygon;
* additional vertices from holes and terrain will be integrated later.
* Use LinkedList for insertion performance.
*/
List<VectorXZ> polygonOutline = new LinkedList<VectorXZ>(polygon.getVertexLoop());
/* integrate holes */
insertHolesInPolygonOutline(polygonOutline, holes);
// if (simplify) {
// * @param simplify if true, the polygon will be modified in a way
// * that improves the robustness of the algorithm,
// * but does not preserve the original shape entirely.
// * This should be used if a first attempt to triangulate failed.
// simplifyOutline(polygonOutline);
// }
/* triangulate */
if (polygonOutline.size() >= 3) {
return triangulateSimplePolygon(polygonOutline);
} else {
return Collections.emptyList();
}
}
/**
* inserts multiple holes into a polygon outline using
* {@link #insertHoleInPolygonOutline(List, SimplePolygonXZ, List)}
*
* TODO: public for debugging purposes
*/
public static void insertHolesInPolygonOutline(
List<VectorXZ> polygonOutline, Collection<SimplePolygonXZ> holes) {
List<SimplePolygonXZ> remainingHoles = new LinkedList<SimplePolygonXZ>(holes);
//It is possible that some holes block other holes' access to the outline.
//However, at least one hole will be inserted during each loop execution.
while (!remainingHoles.isEmpty()) {
for (Iterator<SimplePolygonXZ> holeIt = remainingHoles.iterator(); holeIt.hasNext(); ) {
SimplePolygonXZ hole = (SimplePolygonXZ) holeIt.next();
boolean success = insertHoleInPolygonOutline(polygonOutline, hole, remainingHoles);
if (success) {
holeIt.remove();
}
}
}
}
/**
* @param polygonOutline polygon outline; will be modified directly to perform the insertion
* @param hole hole to be inserted
* @param holes all holes that haven't been integrated into the polygon yet, including 'hole'
* @return true if inserting the hole was successful
*/
static final boolean insertHoleInPolygonOutline(
List<VectorXZ> polygonOutline, SimplePolygonXZ hole, List<SimplePolygonXZ> holes) {
int innerIndex;
Integer outerIndex = null;
List<VectorXZ> holeVertices = hole.getVertices();
for (innerIndex = 0; innerIndex < holeVertices.size(); innerIndex++) {
outerIndex = findVisibleOutlineVertex(polygonOutline,
holeVertices.get(innerIndex), holes);
if (outerIndex != null) { break; }
}
if (outerIndex == null) {
return false;
} else {
SimplePolygonXZ outerPolygon = new SimplePolygonXZ(new ArrayList<VectorXZ>(polygonOutline)); //TODO: avoid creating this every time
polygonOutline.add(outerIndex+1, polygonOutline.get(outerIndex));
if (hole.isClockwise() ^ outerPolygon.isClockwise()) {
polygonOutline.addAll(outerIndex+1,
rearrangeOutline(hole.getVertexLoop(), innerIndex, false));
} else {
polygonOutline.addAll(outerIndex+1,
rearrangeOutline(hole.getVertexLoop(), innerIndex, true));
}
return true;
}
}
/**
* rearranges a polygon outline.
*
* @param outline old polygon outline, will not be modified.
* First and last vertex need to be identical.
* @param newStart index of the new first/last outline vertex
* @param invert determines whether the order of vertices is inverted
* @return New polygon outline.
* The new first and last vertex are identical.
*
*/
static final List<VectorXZ> rearrangeOutline(
List<VectorXZ> outline, int newStart, boolean invert) {
List<VectorXZ> newOutline = new ArrayList<VectorXZ>(outline.size());
boolean complete = false;
boolean oldStartAdded = false;
int currentIndex = newStart;
while (!complete) {
/* add vertex to new outline */
if (currentIndex == 0 || currentIndex == outline.size() - 1) {
if (!oldStartAdded) {//remove original duplication of first/last node
newOutline.add(outline.get(0));
oldStartAdded = true;
}
} else {
newOutline.add(outline.get(currentIndex));
}
/* shift current index */
if (!invert) {
currentIndex += 1;
if (currentIndex >= outline.size()) {
currentIndex = 0;
}
} else {
currentIndex -= 1;
if (currentIndex < 0) {
currentIndex = outline.size() - 1;
}
}
if (currentIndex == newStart) {
complete = true;
}
}
newOutline.add(outline.get(newStart));
return newOutline;
}
static final void insertVertexInPolygonOutline(
List<VectorXZ> polygonOutline, VectorXZ point) {
int index = findVisibleOutlineVertex(polygonOutline, point,
Collections.<SimplePolygonXZ>emptyList());
polygonOutline.add(index+1, point);
polygonOutline.add(index+2, polygonOutline.get(index));
}
/**
* finds a vertex in the polygon outline that is visible from a given point.
* Visibility means that the connection between the point and the outline
* vertex does not intersect the outline or any hole polygon.
*
* @return index of a vertex in the polygon outline
* or null if none was found.
* The method will always find an outline vertex
* if there are no holes.
*/
static final Integer findVisibleOutlineVertex(
List<VectorXZ> polygonOutline, VectorXZ point,
Iterable<SimplePolygonXZ> holes) {
// TODO (performance): replace primitive algorithm with more efficient one
int outerIndex = -1;
checkOuterVertex:
for (VectorXZ outerVertex : polygonOutline) {
outerIndex += 1;
for (int i=0; i+1 < polygonOutline.size(); i++) {
if (null != GeometryUtil.getTrueLineSegmentIntersection(
point, outerVertex,
polygonOutline.get(i), polygonOutline.get(i+1))) {
continue checkOuterVertex;
}
}
for (SimplePolygonXZ hole : holes) {
if (hole.intersects(point, outerVertex)) {
continue checkOuterVertex;
}
}
return outerIndex;
}
return null;
}
/**
* "simplifies" the outline by removing vertices that are (almost)
* on a straight edge, or very close to each other,
* because the triangulation method cannot handle these situations
* - they produce zero-area polygons.
*
* Using this method <em>does</em> affect the result, especially if the
* elevation data associated with the eliminated vertices isn't properly
* represented by linear interpolation between the adjacent vertices.
*/
private static void simplifyOutline(List<VectorXZ> outline) {
outline.remove(outline.size()-1);
Set<Integer> removeIndizes = new HashSet<Integer>();
/* find vertices on straight lines */
for (int i = 0; i < outline.size(); i++) {
if (GeometryUtil.distanceFromLineSegment(
outline.get(i),
new LineSegmentXZ(vertexBefore(outline, i),
vertexAfter(outline, i))) < 0.001) {
removeIndizes.add(i);
}
}
// these didn't have a noticeable effect yet
//
// /* remove two vertices if they are very close and
// * also close to each other's position in the sequence */
//
// for (int i = 0; i < outline.size(); i++) {
// if (VectorXZ.distance(outline.get(i),
// vertexAfter(outline, i)) < 0.001) {
// removeIndizes.add(i);
// }
// }
//
// for (int i = 0; i < outline.size(); i++) {
// if (VectorXZ.distance(vertexBefore(outline, i),
// vertexAfter(outline, i)) < 0.001) {
// removeIndizes.add(indexBefore(outline, i));
// removeIndizes.add(i);
// }
// }
/* actually remove the vertices */
for (int i = outline.size(); i >= 0; i--) {
if (removeIndizes.contains(i)) {
System.out.println("simplify removes vertex #" + i + " in outline: " + outline);
outline.remove(i);
}
}
/* restore the polygon loop property */
outline.add(outline.get(0));
}
/**
* Triangulates a simple polygon using Ear Clipping.
* The implementation is based on the paper
* "Triangulation by Ear Clipping" by David Eberly.
*
* @param outline outline of the polygon to triangulate;
* can be arbitrarily modified by this method.
*/
static final List<TriangleXZ> triangulateSimplePolygon(
List<VectorXZ> outline) {
if (outline.size() == 3) {
return Collections.singletonList(
new TriangleXZ(outline.get(1), outline.get(2), outline.get(3)));
}
outline.remove(0); //TODO: only while first/last vertex is still duplicated
List<TriangleXZ> triangles = new ArrayList<TriangleXZ>(outline.size() - 2);
boolean progress = true;
while(outline.size() >= 3 && progress) {
progress = false;
for (int i=0; i < outline.size(); i++) {
if (isEarTip(i, outline)) {
triangles.add(triangleAtTip(i, outline));
outline.remove(i);
progress = true;
break;
}
}
}
if (outline.size() >= 3) {
throw new InvalidGeometryException("failed to triangulate outline."
+ "\nRemaining: " + outline
+ "\nTriangles: " + triangles);
}
// TODO: try to remove the need for progress check
// TODO (performance) use better algorithm instead of the n^3 above
return triangles;
}
static boolean isEarTip(int i, List<VectorXZ> outline) {
if (isConvex(i, outline)) {
TriangleXZ triangleAtTip = triangleAtTip(i, outline);
for (VectorXZ vertex : outline) {
if (vertex != triangleAtTip.v1
&& vertex != triangleAtTip.v2
&& vertex != triangleAtTip.v3
&& triangleAtTip.contains(vertex)) {
return false;
}
}
return true;
}
return false;
}
/**
* //TODO: outline expects NO duplication of first/last node
*/
static boolean isConvex(int i, List<VectorXZ> outline) {
List<VectorXZ> tempVertices = new ArrayList<VectorXZ>(outline);
tempVertices.add(outline.get(0));
SimplePolygonXZ tempPolygon = new SimplePolygonXZ(tempVertices);
//TODO (performance) avoid creating polygon by passing clockwise information
VectorXZ segBefore = outline.get(i).subtract(vertexBefore(outline, i));
VectorXZ segAfter = vertexAfter(outline, i).subtract(outline.get(i));
return tempPolygon.isClockwise() ^
segBefore.z * segAfter.x - segBefore.x * segAfter.z < 0;
}
static final TriangleXZ triangleAtTip(int i, List<VectorXZ> outline) {
return new TriangleXZ(vertexBefore(outline, i),
outline.get(i), vertexAfter(outline, i));
}
private static final VectorXZ vertexBefore(List<VectorXZ> outline, int i) {
int beforeI = indexBefore(outline, i);
return outline.get(beforeI);
}
private static int indexBefore(List<VectorXZ> outline, int i) {
return (outline.size() + i - 1) % outline.size();
}
private static final VectorXZ vertexAfter(List<VectorXZ> outline, int i) {
int afterI = indexAfter(outline, i);
return outline.get(afterI);
}
private static int indexAfter(List<VectorXZ> outline, int i) {
return (i + 1) % outline.size();
}
/**
* TODO (documentation)
*/
static final Collection<SimplePolygonXZ> splitAlong(SimplePolygonXZ splitPolygon,
Collection<VectorXZ[]> splitLines) {
Collection<SimplePolygonXZ> polygons = new ArrayList<SimplePolygonXZ>(1);
polygons.add(splitPolygon);
for (VectorXZ[] splitLine : splitLines) {
Collection<SimplePolygonXZ> newPolygons = new ArrayList<SimplePolygonXZ>(polygons.size());
for (SimplePolygonXZ polygon : polygons) {
Map<Integer, VectorXZ> intersectionPos;
Map<Integer, LineSegmentXZ> intersectionPartner;
for (int lineVertexI = 0; lineVertexI + 1 < splitLine.length; lineVertexI++) {
}
//TODO: implement (but check whether this method is actually needed first)
}
polygons = newPolygons;
}
return polygons;
}
}