package com.vitco.low.triangulate;
import com.vitco.util.misc.StringIndexer;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Polygon;
import org.jaitools.media.jai.vectorize.VectorizeDescriptor;
import org.poly2tri.Poly2Tri;
import org.poly2tri.geometry.polygon.PolygonPoint;
import org.poly2tri.triangulation.TriangulationAlgorithm;
import org.poly2tri.triangulation.TriangulationContext;
import org.poly2tri.triangulation.TriangulationPoint;
import org.poly2tri.triangulation.delaunay.DelaunayTriangle;
import javax.media.jai.JAI;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.RenderedOp;
import java.awt.image.RenderedImage;
import java.util.*;
/**
* Helper class that converts a grid into triangles.
*
* Uses the surface outline as polygon and then triangulates that.
*
* Allows for optional merging of triangles (this is slow though).
*
* Reference:
* http://code.google.com/p/poly2tri/
*
* Note: The conversion voxel -> polygone with holes uses a very slow implementation
* with heavy resource usage
*/
public final class Grid2TriPolySlow {
// initialize the parameter block
private final static ParameterBlockJAI pb = new ParameterBlockJAI("Vectorize");
static {
pb.setParameter("outsideValues", Collections.singleton(0));
}
// helper - converts "black and white" image into vector representation
@SuppressWarnings("unchecked")
public static Collection<Polygon> doVectorize(RenderedImage src) {
pb.setSource("source0", src);
// Get the desintation image: this is the unmodified source image data
// plus a property for the generated vectors
RenderedOp dest = JAI.create("Vectorize", pb);
pb.removeSources(); // free data (references)
// Get the vectors
Object property = dest.getProperty(VectorizeDescriptor.VECTOR_PROPERTY_NAME);
// Note: this is unchecked (but should be fine) - order doesn't matter
return (Collection<Polygon>)property;
}
// we need only one context for all conversion (faster)
private final static TriangulationContext tcx = Poly2Tri.createContext(TriangulationAlgorithm.DTSweep);
// interpolation value (to avoid duplicate values as poly2tri can't handle those)
public final static float INTERP = 0.000001f;
// helper - method that merges two arraylists at a given point and also interpolates the point
// Note: Assumes that the point only exists once in each list
// Example: (1,2,3,4,5), (6,7,4,9,10), 4 should result in (1,2,3,4,6,7,~4,9,10)
// where the second 4 is interpolated with the interp value from above
// Note: The interpolation is done into the "correct" direction to prevent
// overlap of the areas that are described by the two arraylists
public static ArrayList<PolygonPoint> mergeInterp(ArrayList<PolygonPoint> listA,
ArrayList<PolygonPoint> listB, PolygonPoint p) {
ArrayList<PolygonPoint> result = new ArrayList<PolygonPoint>();
// true once the point was found in the inner list
boolean found = false;
// counter that indicates how many values were inserted from
// the second list after the point was found in it
int count = 0;
// loop over first list
for (int i = 0, size = listA.size(); i < size; i++) {
// add point to new list
PolygonPoint pA = listA.get(i);
result.add(pA);
// check if this is the merge point
if (pA.getX() == p.getX() && pA.getY() == p.getY()) {
// loop over second list
for (PolygonPoint pB : listB) {
if (pB.getX() == p.getX() && pB.getY() == p.getY()) {
// this is the merge point in the inner list
found = true;
} else {
// check if we already found the point in the second list
if (found) {
count++;
result.add(i + count, pB);
} else {
result.add(pB);
}
}
}
// interpolate the second occurrence of the merge point (into the correct direction!)
PolygonPoint refPoint = i + 1 < size ? listA.get(i + 1) : listA.get(0);
double x = pA.getX();
double y = pA.getY();
// interpolate x value if appropriate
if (refPoint.getX() > x) {
x += INTERP;
} else if (refPoint.getX() < x) {
x -= INTERP;
} else {
// interpolate y value
if (refPoint.getY() > y) {
y += INTERP;
} else if (refPoint.getY() < y) {
y -= INTERP;
}
}
// add the interpolated point
result.add(new PolygonPoint(x, y));
}
}
return result;
}
// helper - Merge triangles if they form one big triangle
private static DelaunayTriangle reduce(DelaunayTriangle triA, DelaunayTriangle triB) {
ArrayList<TriangulationPoint> newTri = new ArrayList<TriangulationPoint>();
// compute which values are in both triangles
boolean[] foundA = new boolean[3];
boolean[] foundB = new boolean[3];
int found = 0;
for (int i = 0; i < 3; i++) {
TriangulationPoint p1 = triA.points[i];
newTri.add(p1);
for (int j = 0; j < 3; j++) {
TriangulationPoint p2 = triB.points[j];
if (p1.equals(p2)) {
foundA[i] = true;
foundB[j] = true;
found++;
}
}
}
if (found != 2) {
return null;
}
// create a triangle with four points and check if we can
// merge this into a "real" triangle
// the four point triangle always looks like this: n - f - n - f
for (int i = 0; i < 3; i++) {
if (!foundB[i]) {
if (!foundA[0]) {
newTri.add(2, triB.points[i]);
} else if (!foundA[1]) {
newTri.add(0, triB.points[i]);
} else {
newTri.add(0, newTri.remove(2));
newTri.add(2, triB.points[i]);
}
}
}
// check if we can remove a point
TriangulationPoint p1 = newTri.get(0);
TriangulationPoint p2 = newTri.get(1);
TriangulationPoint p3 = newTri.get(2);
float derivative1 = (p2.getYf() - p1.getYf())/(p2.getXf() - p1.getXf());
float derivative2 = (p3.getYf() - p1.getYf())/(p3.getXf() - p1.getXf());
if (Math.abs(derivative1 - derivative2) < 0.001) {
return new DelaunayTriangle(p1, p3, newTri.get(3));
}
p2 = newTri.get(3);
derivative1 = (p1.getYf() - p2.getYf())/(p1.getXf() - p2.getXf());
derivative2 = (p3.getYf() - p2.getYf())/(p3.getXf() - p2.getXf());
if (Math.abs(derivative1 - derivative2) < 0.001) {
return new DelaunayTriangle(p1, newTri.get(1), p3);
}
return null;
}
// helper - compress the triangles in list (merge what is possible)
private static List<DelaunayTriangle> reduce(List<DelaunayTriangle> toMerge) {
// loop over all entries
for (int i = 0; i < toMerge.size() - 1; i++) {
DelaunayTriangle tri = toMerge.get(i);
// check all neighbours
for (int j = i + 1; j < toMerge.size(); j++) {
DelaunayTriangle triN = toMerge.get(j);
// check if we can merge with the neighbour
DelaunayTriangle merged = reduce(tri, triN);
if (merged != null) {
// set merged triangle and remove neighbour
toMerge.set(i, merged);
toMerge.remove(j);
i--;
break;
}
}
}
return toMerge;
}
// triangulate a polygon
// Note: Since poly2tri has problems with duplicate points in the polygon data
// we need to "fix" that by merging border holes into the polygon outline
// and also merging bordering inside holes. Duplicate points are moved apart
// so that no area intersection is created.
public static ArrayList<DelaunayTriangle> triangulate(Collection<Polygon> polys, boolean triangleReduction) {
ArrayList<DelaunayTriangle> result = new ArrayList<DelaunayTriangle>();
// loop over all polygon (a polygon consists of exterior and interior ring)
for (Polygon poly : polys) {
// stores and manages all seen points
StringIndexer indexer = new StringIndexer();
// stores the "fixed" polygon (zero entry is outside, others are holes)
HashMap<Integer, ArrayList<PolygonPoint>> polygon = new HashMap<Integer, ArrayList<PolygonPoint>>();
// initialize
ArrayList<PolygonPoint> active = new ArrayList<PolygonPoint>();
int activeId = 0;
polygon.put(activeId, active);
// loop over polygon outline (has no clashing points)
Coordinate[] coordinates = poly.getExteriorRing().getCoordinates();
for (int i = 0; i < coordinates.length - 1; i++) {
// add point to list
PolygonPoint point = new PolygonPoint(coordinates[i].x, coordinates[i].y);
active.add(point);
// index the point
indexer.index(point.toString(), activeId);
}
// loop over all holes
for (int n = 0, size = poly.getNumInteriorRing(); n < size; n++) {
// create new active point list
active = new ArrayList<PolygonPoint>();
activeId++;
// not empty iff this point was seen
ArrayList<Integer> seenInList = new ArrayList<Integer>();
ArrayList<PolygonPoint> seenPointsList = new ArrayList<PolygonPoint>();
boolean needToMerge = false;
// loop over all points in this hole
coordinates = poly.getInteriorRingN(n).getCoordinates();
for (int i = 0; i < coordinates.length - 1; i++) {
// add point to list (holes)
PolygonPoint point = new PolygonPoint(coordinates[i].x, coordinates[i].y);
active.add(point);
// check if this needs merging
Integer seenInTmp = indexer.getIndex(point.toString());
if (seenInTmp != null) {
// store all information we need for merging
seenInList.add(seenInTmp);
seenPointsList.add(point);
needToMerge = true;
} else {
// point is unknown, add to index
indexer.index(point.toString(), activeId);
}
}
// merge
if (needToMerge) {
// initial merge (the active list is not stored in "polygon" yet)
// Note: there might be no points indexed yet with activeId (if all points in hole
// were already indexed before!)
int prevSeenIn = seenInList.get(0);
polygon.put(prevSeenIn, mergeInterp(polygon.get(prevSeenIn), active, seenPointsList.get(0)));
indexer.changeIndex(prevSeenIn, activeId);
// merge further seen points
for (int i = 1; i < seenInList.size(); i++) {
// retrieve merge information
Integer seenIn = seenInList.get(i);
PolygonPoint point = seenPointsList.get(i);
// We always merge to lower id. This is required since the lowest id is
// the exterior ring of the polygon.
int mergeTo = Math.min(seenIn, prevSeenIn);
int mergeFrom = Math.max(seenIn, prevSeenIn);
// further merge
polygon.put(mergeTo, mergeInterp(polygon.get(mergeTo), polygon.get(mergeFrom), point));
indexer.changeIndex(mergeTo, mergeFrom);
// update all remaining merges (the index might no longer exist!)
for (int j = i + 1; j < seenInList.size(); j++) {
if (seenInList.get(j) == mergeFrom) {
seenInList.set(j, mergeTo);
}
}
// remove old list
polygon.remove(mergeFrom);
// store the id that we previously merged to (for next merge)
prevSeenIn = mergeTo;
}
} else {
polygon.put(activeId, active);
}
}
// convert to polygon from raw data (zero is always the id that contains the exterior of the polygon)
org.poly2tri.geometry.polygon.Polygon polyR = new org.poly2tri.geometry.polygon.Polygon(polygon.remove(0));
for (ArrayList<PolygonPoint> hole : polygon.values()) {
polyR.addHole(new org.poly2tri.geometry.polygon.Polygon(hole));
}
// do the triangulation and add the triangles
tcx.prepareTriangulation(polyR);
Poly2Tri.triangulate(tcx);
tcx.clear();
if (triangleReduction) {
result.addAll(reduce(polyR.getTriangles()));
} else {
result.addAll(polyR.getTriangles());
}
}
return result;
}
}