In the last episode I presented some code to triangulate a polygon using the ear-clipping algorithm. In this instalment the code has been extended to deal with polygons that have holes. Here are two example results...
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6IvgicsnjzcHzpWk3JcNTmJuxBrr0UeaJ3vix1wUVREaAFalH3JU21bFmoDOkx8Tc7-EV3JTVFRhbY2PuwVwXVNPPyT5EDur7PwG5sXlFgiw0ZxP8NrCJK6NhZWfeJCijnC2D6vjrUH0i/s320/Screen+shot+2010-04-27+at+7.28.45+PM.png)
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_cgPHRZv56inNMPTAE52h4uVSJYmewDfXB_VEHnjYfQ8ac6k-NSEG9npkvriM7ZZW0n1egmK5QTw-22oc5l57AZAKGJFLTlO0D8YVwcRs7fXJaSxgSQnz_J5Cy9HLdeF_lL49ZGaG9dMD/s320/Screen+shot+2010-04-27+at+6.09.10+PM.png)
No, they're not the prettiest triangulations ever seen, but they could be improved with some post-processing.
Here is the Java code used to generate them. Note, you'll need the JTS library if you want to run this. Warning: this code isn't pretty, even by my standards, but it might be useful if you want to play with the algorithm.
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6IvgicsnjzcHzpWk3JcNTmJuxBrr0UeaJ3vix1wUVREaAFalH3JU21bFmoDOkx8Tc7-EV3JTVFRhbY2PuwVwXVNPPyT5EDur7PwG5sXlFgiw0ZxP8NrCJK6NhZWfeJCijnC2D6vjrUH0i/s320/Screen+shot+2010-04-27+at+7.28.45+PM.png)
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_cgPHRZv56inNMPTAE52h4uVSJYmewDfXB_VEHnjYfQ8ac6k-NSEG9npkvriM7ZZW0n1egmK5QTw-22oc5l57AZAKGJFLTlO0D8YVwcRs7fXJaSxgSQnz_J5Cy9HLdeF_lL49ZGaG9dMD/s320/Screen+shot+2010-04-27+at+6.09.10+PM.png)
No, they're not the prettiest triangulations ever seen, but they could be improved with some post-processing.
Here is the Java code used to generate them. Note, you'll need the JTS library if you want to run this. Warning: this code isn't pretty, even by my standards, but it might be useful if you want to play with the algorithm.
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.io.WKTReader;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Demonstrates brute force approach to the ear clipping algorithm
* to triangulate a polygon.
*
* This version attempts a general approach to holes.
*
* @author Michael Bedward
*/
public class EarClipping4 {
private static final double EPS = 1.0E-4;
private final GeometryFactory gf;
private final Polygon inputPolygon;
private Geometry ears;
private int curRingIndex;
private List<Coordinate> shellCoords;
private List<Integer> ringIndices;
/**
* Demonstrate the ear-clipping algorithm
* @throws Exception
*/
public static void main(String[] args) throws Exception {
WKTReader reader = new WKTReader();
Polygon poly = null;
/*
* Square with one hole that can't reach any exterior vertex
*
poly = (Polygon) reader.read(
"POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), " +
"(20 20, 20 40, 40 40, 40 20, 20 20), " +
"(20 160, 20 180, 40 180, 40 160, 20 160), " +
"(160 160, 160 180, 180 180, 180 160, 160 160), " +
"(160 20, 160 40, 180 40, 180 20, 160 20), " +
"(90 90, 90 110, 110 110, 110 90, 90 90) )");
*
*/
/*
* Interlinked U-shaped holes
*
poly = (Polygon) reader.read(
"POLYGON ((0 0, 0 200, 250 200, 250 0, 0 0), " +
"(40 40, 40 170, 60 170, 60 60, 140 60, 140 120, 160 120, 160 40, 40 40), " +
"(90 80, 90 170, 210 170, 210 40, 190 40, 190 150, 110 150, 110 80, 90 80))" );
EarClipping4 clipper = new EarClipping4(poly);
Geometry ears = clipper.getResult();
for (int i = 0; i < ears.getNumGeometries(); i++) {
System.out.println(ears.getGeometryN(i));
}
}
/**
* Constructor
*
* @param inputPolygon the input polygon
*/
public EarClipping4(Polygon inputPolygon) {
gf = new GeometryFactory();
this.inputPolygon = inputPolygon;
}
/**
* Get the result triangular polygons.
*
* @return triangles as a GeometryCollection
*/
public Geometry getResult() {
if (ears == null) {
ears = triangulate();
}
return ears;
}
/**
* Perform the triangulation
*
* @return GeometryCollection of triangular polygons
*/
private Geometry triangulate() {
shellCoords = new ArrayList<Coordinate>();
ringIndices = new ArrayList<Integer>();
List<Polygon> earList = new ArrayList<Polygon>();
connectHoles();
removeColinearVertices();
int N = shellCoords.size() - 1;
boolean finished = false;
boolean found = false;
int k0 = 0;
do {
int k1 = (k0 + 1) % N;
int k2 = (k1 + 1) % N;
LineString ls = gf.createLineString(new Coordinate[] {shellCoords.get(k0), shellCoords.get(k2)});
found = false;
if (inputPolygon.covers(ls)) {
Polygon ear = gf.createPolygon(gf.createLinearRing(
new Coordinate[]{
shellCoords.get(k0),
shellCoords.get(k1),
shellCoords.get(k2),
shellCoords.get(k0)}),
null);
if (inputPolygon.covers(ear)) {
found = true;
for (Polygon p : earList) {
if (ear.overlaps(p)) {
// overlap between candidate ear and previous ear
found = false;
break;
}
}
if (found) {
earList.add(ear);
shellCoords.remove(k1);
ringIndices.remove(k1);
removeColinearVertices();
N = shellCoords.size() - 1;
k0 = 0;
if (N < 3) {
finished = true;
}
}
}
}
if (!found) {
k0++;
if (k0 == N) {
finished = true;
}
}
} while (!finished);
return gf.createGeometryCollection(earList.toArray(new Geometry[0]));
}
/**
* Remove co-linear vertices. TopologyPreservingSimplifier could be
* used for this but that seems like over-kill.
*/
private void removeColinearVertices() {
int N = shellCoords.size() - 1;
List<Coordinate> coordList = new ArrayList<Coordinate>();
for (int j = 1; j <= N; ) {
int i = (j - 1) % N;
int k = (j + 1) % N;
if (ringIndices.get(i) == ringIndices.get(j) &&
ringIndices.get(k) == ringIndices.get(j) &&
CGAlgorithms.isOnLine(shellCoords.get(j),
new Coordinate[]{shellCoords.get(i), shellCoords.get(k)})) {
Coordinate c = shellCoords.remove(j);
ringIndices.remove(j);
N-- ;
} else {
j++ ;
}
}
}
/**
* Connect any holes in the input polygon to the exterior ring to
* form a self-intersecting shell. Holes are added from the lowest
* upwards. As the resulting shell develops, a hole might be added
* to what was originally another hole.
*
* @return a new polygon with holes (if any) connected to the exterior
* ring
*/
private List<Coordinate> connectHoles() {
// defensively copy the input polygon
Polygon poly = (Polygon) inputPolygon.clone();
poly.normalize();
List<Geometry> orderedHoles = getOrderedHoles(poly);
Coordinate[] coords = poly.getExteriorRing().getCoordinates();
shellCoords.addAll(Arrays.asList(coords));
curRingIndex = 0;
for (int i = 0; i < shellCoords.size(); i++) {
ringIndices.add(curRingIndex);
}
for (int i = 0; i < orderedHoles.size(); i++) {
curRingIndex++ ;
joinHoleToShell(orderedHoles.get(i));
}
return shellCoords;
}
/**
* Returns a list of holes in the input polygon (if any) ordered
* by y coordinate with ties broken using x coordinate.
*
* @param poly input polygon
* @return a list of Geometry objects representing the ordered holes
* (may be empty)
*/
private List<Geometry> getOrderedHoles(final Polygon poly) {
List<Geometry> holes = new ArrayList<Geometry>();
List<IndexedEnvelope> bounds = new ArrayList<IndexedEnvelope>();
if (poly.getNumInteriorRing() > 0) {
for (int i = 0; i < poly.getNumInteriorRing(); i++) {
bounds.add( new IndexedEnvelope(i, poly.getInteriorRingN(i).getEnvelopeInternal()) );
}
Collections.sort(bounds, new IndexedEnvelopeComparator());
for (int i = 0; i < bounds.size(); i++) {
holes.add(poly.getInteriorRingN(bounds.get(i).index));
}
}
return holes;
}
/**
* Join a given hole to the current shell. The hole coordinates are
* inserted into the list of shell coordinates.
*
* @param hole the hole to join
*/
private void joinHoleToShell(Geometry hole) {
double minD2 = Double.MAX_VALUE;
int shellVertexIndex = -1;
final int Ns = shellCoords.size() - 1;
final int holeVertexIndex = getLowestVertex(hole);
final Coordinate[] holeCoords = hole.getCoordinates();
final Coordinate ch = holeCoords[holeVertexIndex];
List<IndexedDouble> distanceList = new ArrayList<IndexedDouble>();
/*
* Note: it's important to scan the shell vertices in reverse so
* that if a hole ends up being joined to what was originally
* another hole, the previous hole's coordinates appear in the shell
* before the new hole's coordinates (otherwise the triangulation
* algorithm tends to get stuck).
*/
for (int i = Ns - 1; i >= 0; i--) {
Coordinate cs = shellCoords.get(i);
double d2 = (ch.x - cs.x) * (ch.x - cs.x) + (ch.y - cs.y) * (ch.y - cs.y);
if (d2 < minD2) {
minD2 = d2;
shellVertexIndex = i;
}
distanceList.add(new IndexedDouble(i, d2));
}
/*
* Try a quick join: if the closest shell vertex is reachable without
* crossing any holes.
*/
LineString join = gf.createLineString(new Coordinate[]{ch, shellCoords.get(shellVertexIndex)});
if (inputPolygon.covers(join)) {
doJoinHole(shellVertexIndex, holeCoords, holeVertexIndex);
return;
}
/*
* Quick join didn't work. Sort the shell coords on distance to the
* hole vertex nnd choose the closest reachable one.
*/
Collections.sort(distanceList, new IndexedDoubleComparator());
for (int i = 1; i < distanceList.size(); i++) {
join = gf.createLineString(new Coordinate[] {ch, shellCoords.get(distanceList.get(i).index)});
if (inputPolygon.covers(join)) {
shellVertexIndex = distanceList.get(i).index;
doJoinHole(shellVertexIndex, holeCoords, holeVertexIndex);
return;
}
}
throw new IllegalStateException("Failed to join hole to shell");
}
/**
* Helper method for joinHoleToShell. Insert the hole coordinates into
* the shell coordinate list.
*
* @param shellCoords list of current shell coordinates
* @param shellVertexIndex insertion point in the shell coordinate list
* @param holeCoords array of hole coordinates
* @param holeVertexIndex attachment point of hole
*/
private void doJoinHole(int shellVertexIndex, Coordinate[] holeCoords, int holeVertexIndex) {
List<Coordinate> newCoords = new ArrayList<Coordinate>();
List<Integer> newRingIndices = new ArrayList<Integer>();
newCoords.add(new Coordinate(shellCoords.get(shellVertexIndex)));
newRingIndices.add(ringIndices.get(shellVertexIndex));
final int N = holeCoords.length - 1;
int i = holeVertexIndex;
do {
newCoords.add(new Coordinate(holeCoords[i]));
newRingIndices.add(curRingIndex);
i = (i + 1) % N;
} while (i != holeVertexIndex);
newCoords.add(new Coordinate(holeCoords[holeVertexIndex]));
newRingIndices.add(curRingIndex);
shellCoords.addAll(shellVertexIndex, newCoords);
ringIndices.addAll(shellVertexIndex, newRingIndices);
}
/**
* Return the index of the lowest vertex
*
* @param geom input geometry
* @return index of the first vertex found at lowest point
* of the geometry
*/
private int getLowestVertex(Geometry geom) {
Coordinate[] coords = geom.getCoordinates();
double minY = geom.getEnvelopeInternal().getMinY();
for (int i = 0; i < coords.length; i++) {
if (Math.abs(coords[i].y - minY) < EPS) {
return i;
}
}
throw new IllegalStateException("Failed to find lowest vertex");
}
private static class IndexedEnvelope {
int index;
Envelope envelope;
public IndexedEnvelope(int i, Envelope env) { index = i; envelope = env; }
}
private static class IndexedEnvelopeComparator implements Comparator<IndexedEnvelope> {
public int compare(IndexedEnvelope o1, IndexedEnvelope o2) {
double delta = o1.envelope.getMinY() - o2.envelope.getMinY();
if (Math.abs(delta) < EPS) {
delta = o1.envelope.getMinX() - o2.envelope.getMinX();
if (Math.abs(delta) < EPS) {
return 0;
}
}
return (delta > 0 ? 1 : -1);
}
}
private static class IndexedDouble {
int index;
double value;
public IndexedDouble(int i, double v) { index = i; value = v; }
}
private static class IndexedDoubleComparator implements Comparator<IndexedDouble> {
public int compare(IndexedDouble o1, IndexedDouble o2) {
double delta = o1.value - o2.value;
if (Math.abs(delta) < EPS) {
return 0;
}
return (delta > 0 ? 1 : -1);
}
}
}
HI,Michael,,,,,,,,,,your on packing is great ,,,,,,,i am also working in field of circle packing but in matlab.
ReplyDeleteIf u have supported matlab material then please mail me at
er.sandeepkumarrao@gmail.com