├── .gitignore ├── dist ├── delaunay.jar └── guava-12.0.jar ├── lib └── guava-12.0.jar ├── src └── org │ └── delaunay │ ├── dtfe │ ├── DensityModel.java │ ├── interpolation │ │ ├── InterpolationStrategy.java │ │ ├── InterpolationStrategies.java │ │ ├── MeanInterpolationStrategy.java │ │ ├── NearestNeighborInterpolationStrategy.java │ │ ├── BarycentricLinearInterpolationStrategy.java │ │ ├── AlgebraicLinearInterpolationStrategy.java │ │ └── NaturalNeighborInterpolationStrategy.java │ ├── BasicDensityModel.java │ ├── painters │ │ ├── TriangulationPainterModel.java │ │ ├── DtfePainterModel.java │ │ ├── TriangulationPainter.java │ │ ├── PaintTransform.java │ │ ├── ColorScaleLegendPainter.java │ │ └── DtfePainter.java │ ├── ColorScale.java │ ├── Dtfes.java │ ├── DtfeTriangulationMap.java │ ├── ColorScales.java │ └── IsolineBuilder.java │ ├── model │ ├── Vectors.java │ ├── Edge.java │ ├── Vertex.java │ ├── TriangulationMap.java │ ├── TriangulationMultimap.java │ ├── ConvexPolygon.java │ ├── Vector.java │ ├── Voronoi.java │ └── Triangle.java │ ├── algorithm │ ├── ScaledHilbertIndex.java │ ├── Triangulations.java │ ├── samples │ │ ├── LocateStrategies.java │ │ ├── SampleFunctions.java │ │ └── SampleBuilder.java │ ├── TriangulationLocateTest.java │ ├── HilbertTableIndex.java │ └── Triangulation.java │ ├── PoissonDiscSamplesDemo.java │ ├── Utils.java │ ├── TriangulationDemo.java │ └── ExampleImagesDemo.java ├── .classpath ├── .project ├── LICENSE.txt ├── .settings └── org.eclipse.jdt.core.prefs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.png 3 | -------------------------------------------------------------------------------- /dist/delaunay.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/delaunay/HEAD/dist/delaunay.jar -------------------------------------------------------------------------------- /dist/guava-12.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/delaunay/HEAD/dist/guava-12.0.jar -------------------------------------------------------------------------------- /lib/guava-12.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/delaunay/HEAD/lib/guava-12.0.jar -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/DensityModel.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe; 2 | 3 | public interface DensityModel { 4 | public double getDensity(); 5 | public void setDensity(double d); 6 | public double getWeight(); 7 | } -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/interpolation/InterpolationStrategy.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.interpolation; 2 | 3 | import org.delaunay.dtfe.DensityModel; 4 | import org.delaunay.dtfe.DtfeTriangulationMap; 5 | import org.delaunay.model.Vector; 6 | 7 | public interface InterpolationStrategy { 8 | public double getDensity(DtfeTriangulationMap dtfe, Vector v); 9 | } -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | delaunay 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/org/delaunay/model/Vectors.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | 5 | public class Vectors { 6 | public static Rectangle2D boundingBox(Iterable vs){ 7 | Rectangle2D rect = null; 8 | for(Vector v : vs){ 9 | if(rect == null){ 10 | rect = new Rectangle2D.Double(v.x, v.y, 0, 0); 11 | } else{ 12 | rect.add(v.x, v.y); 13 | } 14 | } 15 | return rect; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/org/delaunay/model/Edge.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | 4 | public class Edge { 5 | public final Vertex a, b; 6 | 7 | public Edge(Vertex a, Vertex b) { 8 | this.a = a; 9 | this.b = b; 10 | } 11 | 12 | @Override 13 | public int hashCode() { 14 | return a.hashCode() ^ b.hashCode(); 15 | } 16 | 17 | @Override 18 | public boolean equals(Object obj) { 19 | if (obj instanceof Edge) { 20 | Edge o = (Edge) obj; 21 | return (a == o.a && b == o.b) || (a == o.b && b == o.a); 22 | } 23 | return false; 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2012 Bill Dwyer 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/BasicDensityModel.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe; 2 | 3 | public class BasicDensityModel implements DensityModel { 4 | protected double density = 0.0; 5 | protected double weight = 1.0; 6 | 7 | public BasicDensityModel() { 8 | this(1.0); 9 | } 10 | 11 | public BasicDensityModel(double weight) { 12 | this.weight = weight; 13 | } 14 | 15 | public double getDensity() { 16 | return density; 17 | } 18 | 19 | public void setDensity(double d) { 20 | this.density = d; 21 | } 22 | 23 | public void setWeight(double weight) { 24 | this.weight = weight; 25 | } 26 | 27 | public double getWeight() { 28 | return weight; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | #Thu May 31 15:48:35 GST 2012 2 | eclipse.preferences.version=1 3 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 4 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 5 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 6 | org.eclipse.jdt.core.compiler.compliance=1.5 7 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 8 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 9 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 10 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 11 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 12 | org.eclipse.jdt.core.compiler.source=1.5 13 | -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/ScaledHilbertIndex.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm; 2 | 3 | import java.awt.Point; 4 | import java.awt.geom.Rectangle2D; 5 | 6 | 7 | public class ScaledHilbertIndex { 8 | private final Rectangle2D scales; 9 | private final HilbertTableIndex index; 10 | 11 | public ScaledHilbertIndex(int order, Rectangle2D bbox) { 12 | long side = 1 << order; 13 | this.scales = new Rectangle2D.Double( 14 | bbox.getMinX(), 15 | bbox.getMinY(), 16 | side / bbox.getWidth(), 17 | side / bbox.getHeight()); 18 | this.index = new HilbertTableIndex(order); 19 | } 20 | 21 | public int toIndex(double x, double y){ 22 | return index.getIndex(new Point( 23 | (int)((x - scales.getX()) * scales.getWidth()), 24 | (int)((y - scales.getY()) * scales.getHeight()))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/interpolation/InterpolationStrategies.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.interpolation; 2 | 3 | 4 | public class InterpolationStrategies { 5 | public static BarycentricLinearInterpolationStrategy createBarycentricLinear() { 6 | return new BarycentricLinearInterpolationStrategy(); 7 | } 8 | 9 | public static AlgebraicLinearInterpolationStrategy createAlgebraicLinear() { 10 | return new AlgebraicLinearInterpolationStrategy(); 11 | } 12 | 13 | public static NearestNeighborInterpolationStrategy createNearestNeighbor() { 14 | return new NearestNeighborInterpolationStrategy(); 15 | } 16 | 17 | public static MeanInterpolationStrategy createMean() { 18 | return new MeanInterpolationStrategy(); 19 | } 20 | 21 | public static NaturalNeighborInterpolationStrategy createNaturalNeighbor() { 22 | return new NaturalNeighborInterpolationStrategy(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/interpolation/MeanInterpolationStrategy.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.interpolation; 2 | 3 | import org.delaunay.dtfe.DensityModel; 4 | import org.delaunay.dtfe.DtfeTriangulationMap; 5 | import org.delaunay.model.Triangle; 6 | import org.delaunay.model.Vector; 7 | import org.delaunay.model.Vertex; 8 | 9 | public class MeanInterpolationStrategy implements InterpolationStrategy { 10 | public double getDensity(DtfeTriangulationMap dtfe, Vector v) { 11 | Triangle tri = dtfe.getTriangulation().locate(v); 12 | 13 | // Do not report density for triangles outside the convex hull of 14 | // map vertices. 15 | if (tri == null || dtfe.getTriangulation().touchesSuperVertex(tri)) { 16 | return 0.0; 17 | } 18 | 19 | double sum = 0.0; 20 | for (Vertex vert : tri.getVertices()) { 21 | sum += dtfe.getDensity(vert); 22 | } 23 | return sum / 3.0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/interpolation/NearestNeighborInterpolationStrategy.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.interpolation; 2 | 3 | import org.delaunay.algorithm.Triangulation.NonDelaunayException; 4 | import org.delaunay.dtfe.DensityModel; 5 | import org.delaunay.dtfe.DtfeTriangulationMap; 6 | import org.delaunay.model.Vector; 7 | import org.delaunay.model.Vertex; 8 | 9 | final class NearestNeighborInterpolationStrategy implements 10 | InterpolationStrategy { 11 | public double getDensity(DtfeTriangulationMap dtfe, Vector v) { 12 | Vertex vert; 13 | try { 14 | vert = dtfe.getTriangulation().locateNearestVertex(v); 15 | } catch (NonDelaunayException e) { 16 | return 0; 17 | } 18 | 19 | // Do not report density for triangles outside the convex hull of 20 | // map vertices. 21 | if (vert == null || dtfe.getTriangulation().neighborsSuperVertex(vert)) { 22 | return 0.0; 23 | } 24 | 25 | return dtfe.getDensity(vert); 26 | } 27 | } -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/Triangulations.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm; 2 | 3 | import java.util.List; 4 | import java.util.Random; 5 | 6 | import org.delaunay.model.Vertex; 7 | 8 | import com.google.common.collect.Lists; 9 | 10 | public class Triangulations { 11 | public static List randomVertices(int n, int width, int height) { 12 | Random random = new Random(System.currentTimeMillis()); 13 | List rands = Lists.newArrayList(); 14 | for (int i = 0; i < n; i++) { 15 | double x = random.nextDouble(); 16 | double y = random.nextDouble(); 17 | x = x*x; 18 | y = y*y; 19 | rands.add(new Vertex(x * width, y * height)); 20 | } 21 | return rands; 22 | } 23 | 24 | public static List randomGaussian(int n, int width, int height) { 25 | Random random = new Random(System.currentTimeMillis()); 26 | List rands = Lists.newArrayList(); 27 | for (int i = 0; i < n; i++) { 28 | double x = random.nextGaussian(); 29 | double y = random.nextGaussian(); 30 | rands.add(new Vertex(x * width / 8 + width / 2, y * width / 8 + height / 2)); 31 | } 32 | return rands; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/painters/TriangulationPainterModel.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.painters; 2 | 3 | import java.awt.Color; 4 | 5 | public class TriangulationPainterModel { 6 | private Color edgeColor = null; 7 | private float edgeStrokeWidth = 1.0f; 8 | 9 | private Color vertexDotColor = null; 10 | private float vertexDotRadius = 2.0f; 11 | 12 | public Color getEdgeColor() { 13 | return edgeColor; 14 | } 15 | 16 | public TriangulationPainterModel setEdgeColor(Color edgeColor) { 17 | this.edgeColor = edgeColor; 18 | return this; 19 | } 20 | 21 | public float getEdgeStrokeWidth() { 22 | return edgeStrokeWidth; 23 | } 24 | 25 | public TriangulationPainterModel setEdgeStrokeWidth(float edgeStrokeWidth) { 26 | this.edgeStrokeWidth = edgeStrokeWidth; 27 | return this; 28 | } 29 | 30 | public Color getVertexDotColor() { 31 | return vertexDotColor; 32 | } 33 | 34 | public TriangulationPainterModel setVertexDotColor(Color vertexColor) { 35 | this.vertexDotColor = vertexColor; 36 | return this; 37 | } 38 | 39 | public float getVertexDotRadius() { 40 | return vertexDotRadius; 41 | } 42 | 43 | public TriangulationPainterModel setVertexDotRadius(float vertexDotRadius) { 44 | this.vertexDotRadius = vertexDotRadius; 45 | return this; 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /src/org/delaunay/model/Vertex.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import java.util.Set; 4 | 5 | import com.google.common.collect.Sets; 6 | 7 | public class Vertex extends Vector { 8 | private final Set neighborVertices = Sets.newLinkedHashSet(); 9 | private final Set neighborTriangles = Sets.newLinkedHashSet(); 10 | private Integer hilbertIndex; 11 | 12 | public Vertex(double x, double y) { 13 | super(x, y); 14 | } 15 | 16 | public Set getNeighborVertices() { 17 | return neighborVertices; 18 | } 19 | 20 | public Set getNeighborTriangles() { 21 | return neighborTriangles; 22 | } 23 | 24 | public void addTriangle(Triangle tri) { 25 | neighborTriangles.add(tri); 26 | neighborVertices.addAll(tri.getVertices()); 27 | neighborVertices.remove(this); 28 | for (Triangle t : neighborTriangles) { 29 | t.invalidateNeighbors(); 30 | } 31 | } 32 | 33 | public void removeTriangle(Triangle tri) { 34 | neighborTriangles.remove(tri); 35 | neighborVertices.removeAll(tri.getVertices()); 36 | for (Triangle t : neighborTriangles) { 37 | t.invalidateNeighbors(); 38 | } 39 | } 40 | 41 | public void setHilbertIndex(Integer hilbertIndex) { 42 | this.hilbertIndex = hilbertIndex; 43 | } 44 | 45 | public Integer getHilbertIndex() { 46 | return hilbertIndex; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/interpolation/BarycentricLinearInterpolationStrategy.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.interpolation; 2 | 3 | import org.delaunay.dtfe.DensityModel; 4 | import org.delaunay.dtfe.DtfeTriangulationMap; 5 | import org.delaunay.model.Triangle; 6 | import org.delaunay.model.Vector; 7 | import org.delaunay.model.Vertex; 8 | 9 | public final class BarycentricLinearInterpolationStrategy implements 10 | InterpolationStrategy { 11 | public double getDensity(DtfeTriangulationMap dtfe, Vector v) { 12 | Triangle tri = dtfe.getTriangulation().locate(v); 13 | 14 | // Do not report density for triangles outside the convex hull of 15 | // map vertices. 16 | if (tri == null || dtfe.getTriangulation().touchesSuperVertex(tri)) { 17 | return 0.0; 18 | } 19 | 20 | Vertex a = tri.a; 21 | Vertex b = tri.b; 22 | Vertex c = tri.c; 23 | 24 | // This method uses barycentric coordinates. 25 | // The only danger here is a divide by zero, which we check. 26 | double det = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y); 27 | if (det == 0.0) { 28 | return 0.0; 29 | } 30 | double lambdaA = ((b.y - c.y) * (v.x - c.x) + (c.x - b.x) * (v.y - c.y)) / det; 31 | double lambdaB = ((c.y - a.y) * (v.x - c.x) + (a.x - c.x) * (v.y - c.y)) / det; 32 | double lambdaC = 1.0 - lambdaA - lambdaB; 33 | double sum = lambdaA * dtfe.getDensity(a) + lambdaB * dtfe.getDensity(b) + lambdaC * dtfe.getDensity(c); 34 | return sum; 35 | } 36 | } -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/interpolation/AlgebraicLinearInterpolationStrategy.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.interpolation; 2 | 3 | import org.delaunay.dtfe.DensityModel; 4 | import org.delaunay.dtfe.DtfeTriangulationMap; 5 | import org.delaunay.model.Triangle; 6 | import org.delaunay.model.Vector; 7 | import org.delaunay.model.Vertex; 8 | 9 | public final class AlgebraicLinearInterpolationStrategy implements 10 | InterpolationStrategy { 11 | public double getDensity(DtfeTriangulationMap dtfe, Vector v) { 12 | Triangle tri = dtfe.getTriangulation().locate(v); 13 | 14 | // Do not report density for triangles outside the convex hull of 15 | // map vertices. 16 | if (tri == null || dtfe.getTriangulation().touchesSuperVertex(tri)) { 17 | return 0.0; 18 | } 19 | 20 | Vertex[] verts = new Vertex[] { tri.a, tri.b, tri.c }; 21 | Vector an = tri.b.normalTo(tri.c).normalize(); 22 | Vector bn = tri.c.normalTo(tri.a).normalize(); 23 | Vector cn = tri.a.normalTo(tri.b).normalize(); 24 | Vector[] norms = new Vector[]{an, bn, cn}; 25 | 26 | double aMax = an.dot(tri.b.subtract(tri.a)); 27 | double bMax = bn.dot(tri.c.subtract(tri.b)); 28 | double cMax = cn.dot(tri.a.subtract(tri.c)); 29 | double[] maxes = new double[]{aMax, bMax, cMax}; 30 | 31 | double sum = 0.0; 32 | for(int i = 0; i < 3; i++){ 33 | double dist = norms[i].dot(v.subtract(verts[i])); 34 | double coeff = 1.0 - dist / maxes[i]; 35 | double dens = coeff * dtfe.getDensity(verts[i]); 36 | sum += dens; 37 | } 38 | return sum; 39 | } 40 | } -------------------------------------------------------------------------------- /src/org/delaunay/model/TriangulationMap.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import org.delaunay.algorithm.Triangulation; 4 | import org.delaunay.algorithm.Triangulation.InvalidVertexException; 5 | 6 | import com.google.common.collect.BiMap; 7 | import com.google.common.collect.HashBiMap; 8 | 9 | /** 10 | * A map that stores the models on vertices of a Delaunay Triangulation. 11 | */ 12 | public class TriangulationMap { 13 | private final BiMap map = HashBiMap.create(); 14 | private final Triangulation triangulation = new Triangulation(); 15 | 16 | public TriangulationMap() { 17 | triangulation.setKeepSuperTriangle(true); 18 | } 19 | 20 | public void clear() { 21 | map.clear(); 22 | triangulation.clear(); 23 | } 24 | 25 | public boolean contains(double x, double y) { 26 | return map.containsKey(new Vertex(x, y)); 27 | } 28 | 29 | public Vertex put(double x, double y, T value) { 30 | Vertex vert = new Vertex(x, y); 31 | triangulation.addVertex(vert); 32 | map.put(vert, value); 33 | return vert; 34 | } 35 | 36 | public void triangulate() throws InvalidVertexException { 37 | triangulation.triangulate(); 38 | } 39 | 40 | public Triangulation getTriangulation() { 41 | return triangulation; 42 | } 43 | 44 | public Vertex getVertex(T value){ 45 | return map.inverse().get(value); 46 | } 47 | 48 | public T get(Vertex v) { 49 | return map.get(v); 50 | } 51 | 52 | public T locate(double x, double y) { 53 | Vertex vert = triangulation.locateNearestVertex(new Vector(x, y)); 54 | return get(vert); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/org/delaunay/model/TriangulationMultimap.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import org.delaunay.algorithm.Triangulation; 7 | import org.delaunay.algorithm.Triangulation.InvalidVertexException; 8 | 9 | import com.google.common.collect.ArrayListMultimap; 10 | import com.google.common.collect.Lists; 11 | import com.google.common.collect.Maps; 12 | 13 | public class TriangulationMultimap { 14 | private final ArrayListMultimap map = ArrayListMultimap.create(); 15 | private final Map canonicalKeys = Maps.newHashMap(); 16 | private final Triangulation triangulation = new Triangulation(); 17 | 18 | public TriangulationMultimap() { 19 | triangulation.setKeepSuperTriangle(true); 20 | } 21 | 22 | public void clear(){ 23 | map.clear(); 24 | triangulation.clear(); 25 | } 26 | 27 | public Triangulation getTriangulation() { 28 | return triangulation; 29 | } 30 | 31 | public boolean contains(double x, double y) { 32 | return map.containsKey(new Vertex(x, y)); 33 | } 34 | 35 | public Vertex put(double x, double y, T value) { 36 | return put(new Vertex(x, y), value); 37 | } 38 | 39 | public Vertex put(Vertex vert, T value) { 40 | if (canonicalKeys.containsKey(vert)) { 41 | vert = canonicalKeys.get(vert); 42 | } else { 43 | triangulation.addVertex(vert); 44 | canonicalKeys.put(vert, vert); 45 | } 46 | map.put(vert, value); 47 | return vert; 48 | } 49 | 50 | public Vertex getKey(double x, double y) { 51 | return canonicalKeys.get(new Vertex(x, y)); 52 | } 53 | 54 | public List get(Vertex key) { 55 | return map.get(key); 56 | } 57 | 58 | public void triangulate() throws InvalidVertexException { 59 | triangulation.triangulate(); 60 | } 61 | 62 | public List locate(double x, double y) { 63 | Vertex vert = triangulation.locateNearestVertex(new Vector(x, y)); 64 | if (vert == null) { 65 | return Lists.newArrayList(); 66 | } 67 | return map.get(vert); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/ColorScale.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe; 2 | 3 | import java.awt.Color; 4 | import java.util.TreeSet; 5 | 6 | import com.google.common.collect.Sets; 7 | 8 | public class ColorScale{ 9 | private final TreeSet stops = Sets.newTreeSet(); 10 | 11 | public static class Stop implements Comparable{ 12 | int a,r,g,b; 13 | int argb; 14 | Double offset; 15 | 16 | private Stop(int argb, Double offset) { 17 | this.argb = argb; 18 | this.a = (argb >> 24) & 0xFF; 19 | this.r = (argb >> 16) & 0xFF; 20 | this.g = (argb >> 8) & 0xFF; 21 | this.b = argb & 0xFF; 22 | this.offset = offset; 23 | } 24 | 25 | public int compareTo(ColorScale.Stop other) { 26 | return offset.compareTo(other.offset); 27 | } 28 | } 29 | 30 | public ColorScale stop(int argb, double value) { 31 | stops.add(new Stop(argb, value)); 32 | return this; 33 | } 34 | 35 | public ColorScale stopAlpha(int rgb, int alpha, double value) { 36 | stops.add(new Stop((rgb & 0xFFFFFF) | (alpha << 24), value)); 37 | return this; 38 | } 39 | 40 | public Color get(Double value){ 41 | if(stops.first().offset.compareTo(value) >= 0){ 42 | return new Color(stops.first().argb, true); 43 | } else if(stops.last().offset.compareTo(value) <= 0){ 44 | return new Color(stops.last().argb, true); 45 | } else{ 46 | ColorScale.Stop s0 = stops.headSet(new Stop(0, value)).last(); 47 | ColorScale.Stop s1 = stops.tailSet(new Stop(0, value)).first(); 48 | double t = (value - s0.offset) / (s1.offset - s0.offset); 49 | int a = 0xFF & (int)(s0.a * (1.0 - t) + s1.a * t); 50 | int r = 0xFF & (int)(s0.r * (1.0 - t) + s1.r * t); 51 | int g = 0xFF & (int)(s0.g * (1.0 - t) + s1.g * t); 52 | int b = 0xFF & (int)(s0.b * (1.0 - t) + s1.b * t); 53 | int argb = (a << 24) | (r << 16) | (g << 8) | b; 54 | return new Color(argb, true); 55 | } 56 | } 57 | 58 | public double getMinStop() { 59 | return stops.first().offset; 60 | } 61 | 62 | public double getMaxStop() { 63 | return stops.last().offset; 64 | } 65 | } -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/painters/DtfePainterModel.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.painters; 2 | 3 | import java.awt.Color; 4 | 5 | import org.delaunay.dtfe.ColorScale; 6 | import org.delaunay.dtfe.ColorScales; 7 | import org.delaunay.dtfe.DtfeTriangulationMap.ScaleType; 8 | import org.delaunay.dtfe.interpolation.InterpolationStrategies; 9 | import org.delaunay.dtfe.interpolation.InterpolationStrategy; 10 | 11 | public class DtfePainterModel { 12 | private ColorScale colorScale = ColorScales.PURPLE_TO_GREEN_LINEAR; 13 | private double densityScalar = 1.0; 14 | private Color edgeColor = null; 15 | private InterpolationStrategy interpolationStrategy = InterpolationStrategies.createNaturalNeighbor(); 16 | private float edgeStrokeWidth = 1.0f; 17 | private ScaleType scaleType = ScaleType.LINEAR; 18 | 19 | public ScaleType getScaleType() { 20 | return scaleType; 21 | } 22 | 23 | public DtfePainterModel setScaleType(ScaleType scaleType) { 24 | this.scaleType = scaleType; 25 | return this; 26 | } 27 | 28 | public float getEdgeStrokeWidth() { 29 | return edgeStrokeWidth; 30 | } 31 | 32 | public DtfePainterModel setEdgeStrokeWidth(float edgeStrokeWidth) { 33 | this.edgeStrokeWidth = edgeStrokeWidth; 34 | return this; 35 | } 36 | 37 | public Color getEdgeColor() { 38 | return edgeColor; 39 | } 40 | 41 | public DtfePainterModel setEdgeColor(Color edgeColor) { 42 | this.edgeColor = edgeColor; 43 | return this; 44 | } 45 | 46 | public ColorScale getColorScale() { 47 | return colorScale; 48 | } 49 | 50 | public DtfePainterModel setColorScale(ColorScale colorScale) { 51 | this.colorScale = colorScale; 52 | return this; 53 | } 54 | 55 | public double getDensityScalar() { 56 | return densityScalar; 57 | } 58 | 59 | public DtfePainterModel setDensityScalar(double densityScalar) { 60 | this.densityScalar = densityScalar; 61 | return this; 62 | } 63 | 64 | public InterpolationStrategy getInterpolationStrategy() { 65 | return interpolationStrategy; 66 | } 67 | 68 | public DtfePainterModel setInterpolationStrategy(InterpolationStrategy interpolationStrategy) { 69 | this.interpolationStrategy = interpolationStrategy; 70 | return this; 71 | } 72 | } -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/samples/LocateStrategies.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm.samples; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.util.List; 5 | 6 | import org.delaunay.Utils; 7 | import org.delaunay.algorithm.Triangulation; 8 | import org.delaunay.algorithm.Triangulation.InvalidVertexException; 9 | import org.delaunay.model.Vector; 10 | import org.delaunay.model.Vertex; 11 | 12 | import com.google.common.base.Function; 13 | import com.google.common.collect.Lists; 14 | 15 | public class LocateStrategies { 16 | public static interface LocateStrategy { 17 | void initialize(Iterable samples, Rectangle2D bounds); 18 | boolean addSample(Vector v); 19 | Vector getNearest(Vector v); 20 | } 21 | 22 | public static class NaiveLocateStrategy implements LocateStrategy { 23 | private List locatable = Lists.newArrayList(); 24 | 25 | public void initialize(Iterable samples, Rectangle2D bounds) { 26 | locatable = Lists.newArrayList(samples); 27 | } 28 | 29 | public boolean addSample(Vector v) { 30 | locatable.add(v); 31 | return true; 32 | } 33 | 34 | public Vector getNearest(final Vector v) { 35 | return Utils.minObject(locatable, new Function() { 36 | public Double apply(Vector vert) { 37 | return vert.subtract(v).lengthSquared(); 38 | } 39 | }); 40 | } 41 | } 42 | 43 | public static class TriangulationLocateStrategy implements LocateStrategy { 44 | private Triangulation triangulation = new Triangulation(); 45 | 46 | public void initialize(Iterable samples, Rectangle2D bounds) { 47 | triangulation = new Triangulation(); 48 | triangulation.createSuperTriangle(bounds); 49 | for (Vector v : samples) { 50 | addSample(v); 51 | } 52 | } 53 | 54 | public boolean addSample(Vector v) { 55 | try { 56 | triangulation.addVertexToTriangulation(new Vertex(v.x, v.y)); 57 | return true; 58 | } catch (InvalidVertexException e) { 59 | return false; 60 | } 61 | } 62 | 63 | public Vector getNearest(final Vector v) { 64 | return triangulation.locateNearestVertex(v); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/org/delaunay/model/ConvexPolygon.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import java.util.List; 4 | 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.common.collect.Lists; 7 | 8 | public strictfp class ConvexPolygon { 9 | private Double area = null; 10 | private final List vertices; 11 | 12 | public ConvexPolygon(Iterable vertices) { 13 | this.vertices = ImmutableList.copyOf(vertices); 14 | } 15 | 16 | public List getVertices() { 17 | return vertices; 18 | } 19 | 20 | public double getArea() { 21 | if (area != null) { 22 | return area; 23 | } 24 | area = 0.0; 25 | 26 | if(vertices.size() > 0){ 27 | Vector c = vertices.get(0); 28 | 29 | for (int i = 1; i < vertices.size(); i++) { 30 | // compute area of triangle defined by vectors a, b, c 31 | Vector a = vertices.get(i - 1); 32 | Vector b = vertices.get(i); 33 | if (c == a) { 34 | continue; 35 | } 36 | 37 | area += Math.abs(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) / 2.0; 38 | } 39 | } 40 | return area; 41 | } 42 | 43 | public ConvexPolygon intersect(ConvexPolygon o) { 44 | List output = Lists.newArrayList(o.getVertices()); 45 | 46 | // http://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm 47 | for (int i = 0; i < vertices.size(); i++) { 48 | Vector a = i == 0 ? vertices.get(vertices.size() - 1) : vertices.get(i - 1); 49 | Vector b = vertices.get(i); 50 | 51 | List input = output; 52 | output = Lists.newArrayList(); 53 | 54 | for (int j = 0; j < input.size(); j++) { 55 | Vector c = j == 0 ? input.get(input.size() - 1) : input.get(j - 1); 56 | Vector d = input.get(j); 57 | 58 | if (c.orientation(a, b) >= 0) { 59 | output.add(c); 60 | if (d.orientation(a, b) < 0) { 61 | output.add(intersection(a, b, c, d)); 62 | } 63 | } else if (d.orientation(a, b) >= 0) { 64 | output.add(intersection(a, b, c, d)); 65 | } 66 | } 67 | } 68 | return new ConvexPolygon(output); 69 | } 70 | 71 | public Vector intersection(Vector a, Vector b, Vector c, Vector d) { 72 | Vector p = a; 73 | Vector r = b.subtract(a); 74 | Vector q = c; 75 | Vector s = d.subtract(c); 76 | double t = (q.subtract(p)).cross(s) / r.cross(s); 77 | return p.add(r.multiply(t)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/painters/TriangulationPainter.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.painters; 2 | 3 | import java.awt.BasicStroke; 4 | import java.awt.Graphics2D; 5 | import java.awt.Point; 6 | import java.awt.RenderingHints; 7 | import java.awt.image.BufferedImage; 8 | import java.util.Set; 9 | 10 | import org.delaunay.algorithm.Triangulation; 11 | import org.delaunay.model.Edge; 12 | import org.delaunay.model.Triangle; 13 | import org.delaunay.model.Vertex; 14 | 15 | import com.google.common.collect.Iterables; 16 | import com.google.common.collect.Sets; 17 | 18 | public class TriangulationPainter { 19 | private final TriangulationPainterModel model; 20 | 21 | public TriangulationPainter(TriangulationPainterModel model) { 22 | this.model = model; 23 | } 24 | 25 | public BufferedImage paint( 26 | Triangulation triangulation, 27 | PaintTransform transform) { 28 | BufferedImage img = new BufferedImage(transform.getWidth(), transform.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); 29 | Graphics2D g = (Graphics2D) img.getGraphics(); 30 | 31 | // Draw Edges 32 | if (model.getEdgeColor() != null) { 33 | g.setColor(model.getEdgeColor()); 34 | g.setStroke(new BasicStroke(model.getEdgeStrokeWidth())); 35 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 36 | 37 | for (Edge e : getPaintableEdges(triangulation)) { 38 | Point a = transform.toImagePoint(e.a); 39 | Point b = transform.toImagePoint(e.b); 40 | g.drawLine(a.x, a.y, b.x, b.y); 41 | } 42 | } 43 | 44 | if (model.getVertexDotColor() != null) { 45 | g.setColor(model.getVertexDotColor()); 46 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 47 | float r = model.getVertexDotRadius(); 48 | 49 | for (Vertex v : triangulation.getVertices()) { 50 | Point p = transform.toImagePoint(v); 51 | g.fillOval((int)(p.x - r), (int)(p.y - r), (int)(r * 2), (int)(r * 2)); 52 | } 53 | } 54 | 55 | return img; 56 | } 57 | 58 | public static Set getPaintableEdges(Triangulation triangulation) { 59 | Set allEdges = Sets.newHashSet(); 60 | for (Triangle tri : triangulation.getTriangles()) { 61 | if (triangulation.touchesSuperVertex(tri)) { 62 | continue; 63 | } 64 | Iterables.addAll(allEdges, tri.getEdges()); 65 | } 66 | return allEdges; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/org/delaunay/PoissonDiscSamplesDemo.java: -------------------------------------------------------------------------------- 1 | package org.delaunay; 2 | 3 | import java.awt.Color; 4 | import java.awt.Rectangle; 5 | import java.awt.image.BufferedImage; 6 | import java.io.File; 7 | 8 | import javax.imageio.ImageIO; 9 | 10 | import org.delaunay.algorithm.Triangulation; 11 | import org.delaunay.algorithm.samples.LocateStrategies.TriangulationLocateStrategy; 12 | import org.delaunay.algorithm.samples.SampleBuilder; 13 | import org.delaunay.algorithm.samples.SampleFunctions.PoissonDiscSampleFunction; 14 | import org.delaunay.algorithm.samples.SampleFunctions.SampleFunction; 15 | import org.delaunay.algorithm.samples.SampleFunctions.VariablePoissonDiscSampleFunction; 16 | import org.delaunay.dtfe.painters.PaintTransform; 17 | import org.delaunay.dtfe.painters.TriangulationPainter; 18 | import org.delaunay.dtfe.painters.TriangulationPainterModel; 19 | import org.delaunay.model.Vector; 20 | 21 | /** 22 | * Generates an image of point samples. 23 | * 24 | * Each new sample is computed using a poisson disc distribution around an 25 | * existing sample point. 26 | * 27 | * @see http://en.wikipedia.org/wiki/Supersampling 28 | */ 29 | public class PoissonDiscSamplesDemo { 30 | @SuppressWarnings("unused") 31 | public static void main(String[] args) throws Exception { 32 | // Create poisson disc sample function 33 | Rectangle bounds = new Rectangle(0, 0, 400, 400); 34 | SampleFunction poissonFunction = new PoissonDiscSampleFunction(bounds, 10); 35 | SampleFunction variableFunction = new VariablePoissonDiscSampleFunction(bounds) { 36 | public double getMimimumDistance(Vector v) { 37 | return 10 + v.x / 5; 38 | } 39 | }; 40 | 41 | // Generate samples to fill bounds 42 | Iterable samples = new SampleBuilder() 43 | .setLocateStrategy(new TriangulationLocateStrategy()) 44 | .fill(poissonFunction) 45 | .getSamples(); 46 | 47 | // Triangulate and paint 48 | Triangulation t = new Triangulation(); 49 | t.addAllVertices(Utils.toVertices(samples)); 50 | t.triangulate(); 51 | 52 | TriangulationPainter painter = new TriangulationPainter( 53 | new TriangulationPainterModel().setVertexDotColor(Color.BLACK)); 54 | BufferedImage img = painter.paint(t, new PaintTransform(bounds.width, bounds.height)); 55 | ImageIO.write(img, "png", new File("poisson.png")); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/painters/PaintTransform.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.painters; 2 | 3 | import java.awt.Point; 4 | import java.awt.Rectangle; 5 | import java.awt.geom.Point2D; 6 | import java.awt.geom.Rectangle2D; 7 | import java.util.List; 8 | 9 | import org.delaunay.model.Vector; 10 | 11 | import com.google.common.base.Function; 12 | import com.google.common.collect.Iterables; 13 | import com.google.common.collect.Lists; 14 | 15 | public class PaintTransform { 16 | private final int width; 17 | private final int height; 18 | private final Rectangle2D dtfeWindow; 19 | 20 | public PaintTransform(int width, int height) { 21 | this(width, height, new Rectangle(0, 0, width, height)); 22 | } 23 | 24 | public PaintTransform(int width, int height, Rectangle2D dtfeWindow) { 25 | this.width = width; 26 | this.height = height; 27 | this.dtfeWindow = dtfeWindow; 28 | } 29 | 30 | public List createSlices(int slices) { 31 | int heightPerSlice = height / slices; 32 | List transforms = Lists.newArrayList(); 33 | for(int i = 0; i < slices; i++){ 34 | transforms.add(new PaintTransform( 35 | width, 36 | heightPerSlice, 37 | new Rectangle2D.Double( 38 | dtfeWindow.getMinX(), 39 | dtfeWindow.getMinY() + i * dtfeWindow.getHeight() / slices, 40 | dtfeWindow.getWidth(), 41 | dtfeWindow.getHeight() / slices))); 42 | } 43 | return transforms; 44 | } 45 | 46 | public int getWidth() { 47 | return width; 48 | } 49 | 50 | public int getHeight() { 51 | return height; 52 | } 53 | 54 | public Rectangle2D getDtfeWindow() { 55 | return dtfeWindow; 56 | } 57 | 58 | public Vector toDtfeVector(int x, int y){ 59 | return new Vector( 60 | dtfeWindow.getMinX() + dtfeWindow.getWidth() * x / width, 61 | dtfeWindow.getMinY() + dtfeWindow.getHeight() * y / height); 62 | } 63 | 64 | public Point toImagePoint(Vector v) { 65 | double x = (v.x - dtfeWindow.getMinX()) * width / dtfeWindow.getWidth(); 66 | double y = (v.y - dtfeWindow.getMinY()) * height / dtfeWindow.getHeight(); 67 | return new Point((int) x, (int) y); 68 | } 69 | 70 | public Iterable toImagePoints(Iterable vectors){ 71 | return Iterables.transform(vectors, new Function() { 72 | public Point2D apply(Vector vector) { 73 | return toImagePoint(vector); 74 | } 75 | }); 76 | } 77 | } -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/painters/ColorScaleLegendPainter.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.painters; 2 | 3 | import java.awt.Rectangle; 4 | import java.awt.image.BufferedImage; 5 | import java.util.Arrays; 6 | 7 | import org.delaunay.dtfe.ColorScale; 8 | 9 | public class ColorScaleLegendPainter { 10 | public static enum Orientation{ 11 | HORIZONTAL, 12 | VERTICAL; 13 | } 14 | 15 | public static class ColorScalePainterModel { 16 | private Orientation orientation = Orientation.HORIZONTAL; 17 | private Rectangle bounds = new Rectangle(0, 0, 300, 20); 18 | 19 | public Orientation getOrientation() { 20 | return orientation; 21 | } 22 | 23 | public ColorScalePainterModel setOrientation(Orientation orientation) { 24 | this.orientation = orientation; 25 | return this; 26 | } 27 | 28 | public Rectangle getBounds() { 29 | return bounds; 30 | } 31 | 32 | public ColorScalePainterModel setBounds(Rectangle bounds) { 33 | this.bounds = bounds; 34 | return this; 35 | } 36 | } 37 | 38 | private final ColorScalePainterModel model; 39 | 40 | public ColorScaleLegendPainter(ColorScalePainterModel model) { 41 | this.model = model; 42 | } 43 | 44 | public BufferedImage createLegendImage(ColorScale scale){ 45 | BufferedImage img = new BufferedImage(model.getBounds().width, model.getBounds().height, BufferedImage.TYPE_4BYTE_ABGR); 46 | paintLegendImage(scale, img); 47 | return img; 48 | } 49 | 50 | public void paintLegendImage(ColorScale scale, BufferedImage img) { 51 | Rectangle bounds = model.getBounds(); 52 | double s0 = scale.getMinStop(); 53 | double s1 = scale.getMaxStop(); 54 | double ds = s1 - s0; 55 | 56 | if (model.getOrientation() == Orientation.VERTICAL) { 57 | int[] rgb = new int[bounds.width]; 58 | for (int y = 0; y < bounds.height; y++) { 59 | double value = 1.0 - (double) y / bounds.height; 60 | value = s0 + ds * value; 61 | Arrays.fill(rgb, scale.get(value).getRGB()); 62 | img.setRGB(bounds.x, bounds.y + y, bounds.width, 1, rgb, 0, bounds.height); 63 | } 64 | } else { 65 | int[] rgb = new int[bounds.width]; 66 | for (int x = 0; x < bounds.width; x++) { 67 | double value = (double) x / bounds.width; 68 | value = s0 + ds * value; 69 | rgb[x] = scale.get(value).getRGB(); 70 | } 71 | 72 | for (int y = 0; y < bounds.height; y++) { 73 | img.setRGB(bounds.x, bounds.y + y, bounds.width, 1, rgb, 0, bounds.height); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/org/delaunay/model/Vector.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import java.awt.geom.Point2D; 4 | 5 | public strictfp class Vector { 6 | public final double x, y; 7 | 8 | public Vector(double x, double y) { 9 | this.x = x; 10 | this.y = y; 11 | } 12 | 13 | public Vector multiply(double s) { 14 | return new Vector(x * s, y * s); 15 | } 16 | 17 | public Vector divide(double s) { 18 | return new Vector(x / s, y / s); 19 | } 20 | 21 | public Vector subtract(Vector v) { 22 | return new Vector(x - v.x, y - v.y); 23 | } 24 | 25 | public Vector add(Vector v) { 26 | return new Vector(x + v.x, y + v.y); 27 | } 28 | 29 | public double dot(Vector v){ 30 | return (x * v.x) + (y * v.y); 31 | } 32 | 33 | public double cross(Vector v){ 34 | return (x * v.y) - (y * v.x); 35 | } 36 | 37 | public double lengthSquared(){ 38 | return dot(this); 39 | } 40 | 41 | public double length(){ 42 | return Math.sqrt(lengthSquared()); 43 | } 44 | 45 | public Vector normalize(){ 46 | double l = length(); 47 | return new Vector(x/l, y/l); 48 | } 49 | 50 | public Vector normalTo(Vector v){ 51 | return new Vector(y - v.y, v.x - x); 52 | } 53 | 54 | /* 55 | * Does this vector lie on the left or right of ab? 56 | * -1 = left 57 | * 0 = on 58 | * 1 = right 59 | */ 60 | public int orientation(Vector a, Vector b) { 61 | double det = (a.x - x) * (b.y - y) - (b.x - x) * (a.y - y); 62 | return (new Double(det).compareTo(0.0)); 63 | } 64 | 65 | public Point2D.Double toPoint() { 66 | return new Point2D.Double(x, y); 67 | } 68 | 69 | public String toString(){ 70 | return String.format("(%.4f, %.4f)", x, y); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | final int prime = 31; 76 | int result = 1; 77 | long temp; 78 | temp = Double.doubleToLongBits(x); 79 | result = prime * result + (int) (temp ^ (temp >>> 32)); 80 | temp = Double.doubleToLongBits(y); 81 | result = prime * result + (int) (temp ^ (temp >>> 32)); 82 | return result; 83 | } 84 | 85 | @Override 86 | public boolean equals(Object obj) { 87 | if (this == obj) 88 | return true; 89 | if (obj == null) 90 | return false; 91 | if (getClass() != obj.getClass()) 92 | return false; 93 | Vector other = (Vector) obj; 94 | if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x)) 95 | return false; 96 | if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y)) 97 | return false; 98 | return true; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/interpolation/NaturalNeighborInterpolationStrategy.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.interpolation; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import org.delaunay.algorithm.Triangulation; 9 | import org.delaunay.algorithm.Triangulation.InvalidVertexException; 10 | import org.delaunay.algorithm.Triangulation.NonDelaunayException; 11 | import org.delaunay.dtfe.DensityModel; 12 | import org.delaunay.dtfe.DtfeTriangulationMap; 13 | import org.delaunay.model.Triangle; 14 | import org.delaunay.model.Vector; 15 | import org.delaunay.model.Vertex; 16 | import org.delaunay.model.Voronoi; 17 | 18 | import com.google.common.collect.Maps; 19 | import com.google.common.collect.Sets; 20 | 21 | public class NaturalNeighborInterpolationStrategy implements InterpolationStrategy { 22 | private final Map voronoi; 23 | 24 | public NaturalNeighborInterpolationStrategy() { 25 | this.voronoi = Maps.newLinkedHashMap(); 26 | } 27 | 28 | public Voronoi getVoronoi(Vertex v) { 29 | if (voronoi.containsKey(v)) { 30 | return voronoi.get(v); 31 | } 32 | 33 | Voronoi cell = Voronoi.createFromTriangulation(v); 34 | voronoi.put(v, cell); 35 | return cell; 36 | } 37 | 38 | public Voronoi getSecondOrderVoronoi(Triangulation triangulation, Vertex v) throws NonDelaunayException, InvalidVertexException { 39 | Collection cavity = triangulation.getCircumcircleTriangles(v); 40 | List tris = triangulation.createTriangles(triangulation.getEdgeSet(cavity), v); 41 | Set verts = Sets.newHashSet(); 42 | for (Triangle tri : tris) { 43 | verts.addAll(tri.getVertices()); 44 | } 45 | verts.remove(v); 46 | return Voronoi.create(v, verts, tris); 47 | } 48 | 49 | public double getDensity(DtfeTriangulationMap dtfe, Vector v) { 50 | Triangle tri = dtfe.getTriangulation().locate(v); 51 | if (tri == null || dtfe.getTriangulation().touchesSuperVertex(tri)) { 52 | return 0.0; 53 | } 54 | 55 | try { 56 | Voronoi vor = getSecondOrderVoronoi(dtfe.getTriangulation(), new Vertex(v.x, v.y)); 57 | double area = 0; 58 | for (Vertex vert : vor.getNeighborVertices()) { 59 | if (dtfe.getTriangulation().neighborsSuperVertex(vert)) { 60 | continue; 61 | } 62 | Voronoi vertVor = getVoronoi(vert); 63 | if (vertVor != null) { 64 | area += vor.intersect(vertVor).getArea() * dtfe.getDensity(vert); 65 | } 66 | } 67 | return area / vor.getArea(); 68 | } catch (Exception e) { 69 | e.printStackTrace(); 70 | return 0; 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/painters/DtfePainter.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe.painters; 2 | 3 | import java.awt.BasicStroke; 4 | import java.awt.Graphics2D; 5 | import java.awt.Point; 6 | import java.awt.RenderingHints; 7 | import java.awt.image.BufferedImage; 8 | 9 | import org.delaunay.dtfe.DensityModel; 10 | import org.delaunay.dtfe.DtfeTriangulationMap; 11 | import org.delaunay.model.Edge; 12 | import org.delaunay.model.Vector; 13 | 14 | import com.google.common.base.Function; 15 | import com.google.common.collect.Iterables; 16 | 17 | public class DtfePainter { 18 | 19 | private final DtfePainterModel model; 20 | 21 | public DtfePainter(DtfePainterModel model) { 22 | this.model = model; 23 | } 24 | 25 | public Iterable paintSlices( 26 | final DtfeTriangulationMap dtfe, 27 | PaintTransform transform, 28 | int slices) { 29 | 30 | return Iterables.transform(transform.createSlices(slices), 31 | new Function() { 32 | public BufferedImage apply(PaintTransform pattern) { 33 | return paint(dtfe, pattern); 34 | } 35 | }); 36 | } 37 | 38 | 39 | public BufferedImage paint( 40 | DtfeTriangulationMap dtfe, 41 | PaintTransform transform) { 42 | BufferedImage img = new BufferedImage(transform.getWidth(), transform.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); 43 | Graphics2D g = (Graphics2D) img.getGraphics(); 44 | 45 | // Fill scanlines 46 | for (int y = 0; y < transform.getHeight(); y++) { 47 | double[] scanline = new double[transform.getWidth()]; 48 | for (int x = 0; x < transform.getWidth(); x++) { 49 | Vector v = transform.toDtfeVector(x, y); 50 | scanline[x] = dtfe.getInterpolatedDensity(v, model.getInterpolationStrategy()); 51 | } 52 | int[] rgb = new int[transform.getWidth()]; 53 | for (int x = 0; x < transform.getWidth(); x++) { 54 | double scale = model.getDensityScalar() * dtfe.getRelativeDensity(scanline[x], model.getScaleType()); 55 | rgb[x] = model.getColorScale().get(scale).getRGB(); 56 | } 57 | img.setRGB(0, y, transform.getWidth(), 1, rgb, 0, transform.getWidth()); 58 | } 59 | 60 | // Draw Edges 61 | if (model.getEdgeColor() != null) { 62 | g.setColor(model.getEdgeColor()); 63 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 64 | g.setStroke(new BasicStroke(model.getEdgeStrokeWidth())); 65 | 66 | for (Edge e : TriangulationPainter.getPaintableEdges(dtfe.getTriangulation())) { 67 | Point a = transform.toImagePoint(e.a); 68 | Point b = transform.toImagePoint(e.b); 69 | g.drawLine(a.x, a.y, b.x, b.y); 70 | } 71 | } 72 | return img; 73 | } 74 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | delaunay 2 | ======== 3 | 4 | Delaunay Triangulation for Java with Hilbert Curve linearization and a Delaunay Tesselation Field Estimator (DTFE) 5 | 6 | ### Four line demo ### 7 | ```Java 8 | public static void fourLiner() throws Exception { 9 | Triangulation t = new Triangulation(); 10 | t.addAllVertices(Triangulations.randomVertices(1000, 400, 400)); 11 | t.triangulate(); 12 | Demo.drawTriangulation(t, 400, 400, "triangulation.png"); 13 | } 14 | ``` 15 | 16 | ### Delaunay Triangulation ### 17 | The `Triangulation` class creates a Delaunay Triangulation of the Vertexs. (http://en.wikipedia.org/wiki/Delaunay_triangulation) 18 | 19 | This implementation uses a simple iterative approach, but with some pre-processing 20 | to make the real-world performance fast. 21 | 22 | 1. For each vertex, we walk to the enclosing triangle. 23 | 2. We create a cavity from that triangle and all neighboring triangles for which the vertex is 24 | in its circumcircle. 25 | 3. We create new triangles between the edges of the cavity and the 26 | vertex. 27 | 28 | The basic incremental triangulation method inspired by Paul Bourke's notes 29 | and psuedocode. See: (http://paulbourke.net/papers/triangulate/). 30 | 31 | ### Performance Characteristics ### 32 | Prior to triangulation, the vertices are sorted using a Hilbert Space-Filling curve (http://en.wikipedia.org/wiki/Hilbert_curve). Since 33 | our locate method walks the triangulation, linearizing the points with a 34 | space-filling curve gives us some pretty good locality when adding each 35 | vertex, thus greatly reducing the number of hops required to locate the 36 | vertex. The sort is O(n log n), but is fast since hilbert indices are 37 | computed in O(h) (where h is a small constant), and results in a 38 | triangulation asymptotic running time of O(n) for non-diabolical cases. 39 | 40 | ### Delaunay Tessellation Field Estimator ### 41 | The `DtfeTriangulationMap` class performs the **Delaunay Tessellation Field Estimator** (DTFE) (http://en.wikipedia.org/wiki/Delaunay_tessellation_field_estimator) in two dimensions, which enables the reconstruction of the continuous density field from a set of points. 42 | 43 | The DTFE is simple to understand: 44 | 45 | 1. Construct a triangulation of the points. 46 | 2. For each vertex, compute its density with the formula: density = point_mass / sum_of_area_of_neighboring_triangles 47 | 3. To reconstruct the continuous field, interpolate the density using the vertex densities. 48 | 49 | There are several methods for interpolation, which are included in the `InterpolationStrategies` class. 50 | 51 | For more info and **PICTURES!** check out this wiki page: 52 | (https://github.com/themadcreator/delaunay/wiki/DTFE-Interpolation-Strategies) -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/Dtfes.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe; 2 | 3 | import java.util.LinkedHashSet; 4 | 5 | import org.delaunay.algorithm.Triangulation.InvalidVertexException; 6 | import org.delaunay.dtfe.interpolation.InterpolationStrategy; 7 | import org.delaunay.model.Vertex; 8 | 9 | import com.google.common.collect.Sets; 10 | 11 | public class Dtfes { 12 | public static class DifferenceDensityModel extends BasicDensityModel { 13 | private final T aChild; 14 | private final T bChild; 15 | private final double densityDiff; 16 | 17 | public DifferenceDensityModel(T aChild, T bChild, double density) { 18 | this.aChild = aChild; 19 | this.bChild = bChild; 20 | this.densityDiff = density; 21 | } 22 | 23 | @Override 24 | public double getDensity() { 25 | return densityDiff; 26 | } 27 | 28 | public T getAChild() { 29 | return aChild; 30 | } 31 | 32 | public T getBChild() { 33 | return bChild; 34 | } 35 | } 36 | 37 | public static DtfeTriangulationMap> difference( 38 | DtfeTriangulationMap a, 39 | DtfeTriangulationMap b, 40 | InterpolationStrategy interpolation) throws InvalidVertexException { 41 | 42 | // Compute vertex partitions 43 | LinkedHashSet averts = a.getTriangulation().getVertices(); 44 | LinkedHashSet bverts = b.getTriangulation().getVertices(); 45 | 46 | LinkedHashSet aOnly = Sets.newLinkedHashSet(averts); 47 | aOnly.removeAll(bverts); 48 | 49 | LinkedHashSet bOnly = Sets.newLinkedHashSet(bverts); 50 | bOnly.removeAll(averts); 51 | 52 | LinkedHashSet aAndB = Sets.newLinkedHashSet(averts); 53 | aAndB.retainAll(bverts); 54 | 55 | // Generate difference map 56 | DtfeTriangulationMap> diff = new DtfeTriangulationMap>(); 57 | for (Vertex v : aOnly) { 58 | T aChild = a.get(v); 59 | double bDense = b.getInterpolatedDensity(v, interpolation); 60 | diff.put(v.x, v.y, new DifferenceDensityModel( 61 | aChild, 62 | null, 63 | aChild.getDensity() - bDense)); 64 | } 65 | for (Vertex v : bOnly) { 66 | T bChild = b.get(v); 67 | double aDense = a.getInterpolatedDensity(v, interpolation); 68 | diff.put(v.x, v.y, new DifferenceDensityModel( 69 | null, 70 | bChild, 71 | aDense - bChild.getDensity())); 72 | } 73 | for (Vertex v : aAndB) { 74 | T aChild = a.get(v); 75 | T bChild = b.get(v); 76 | diff.put(v.x, v.y, new DifferenceDensityModel( 77 | aChild, 78 | bChild, 79 | aChild.getDensity() - bChild.getDensity())); 80 | } 81 | diff.triangulate(); 82 | return diff; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/samples/SampleFunctions.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm.samples; 2 | 3 | import java.awt.Shape; 4 | import java.awt.geom.Rectangle2D; 5 | import java.util.Random; 6 | 7 | import org.delaunay.model.Vector; 8 | 9 | public class SampleFunctions { 10 | 11 | public static interface SampleFunction { 12 | Shape getBoundingShape(); 13 | Vector createSampleIn(Shape shape); 14 | Vector createSampleNear(Vector v); 15 | double getMimimumDistance(Vector v); 16 | } 17 | 18 | public static abstract class BoundSampleFunction implements SampleFunction { 19 | protected Random random = new Random(System.currentTimeMillis()); 20 | protected final Shape shape; 21 | 22 | public BoundSampleFunction(Shape shape) { 23 | this.shape = shape; 24 | } 25 | 26 | public Shape getBoundingShape() { 27 | return shape; 28 | } 29 | 30 | public Vector createSampleIn(Shape shape) { 31 | Rectangle2D b = shape.getBounds2D(); 32 | double x = b.getMinX() + random.nextDouble() * b.getWidth(); 33 | double y = b.getMinY() + random.nextDouble() * b.getHeight(); 34 | return new Vector(x, y); 35 | } 36 | } 37 | 38 | public static class PoissonDiscSampleFunction extends BoundSampleFunction { 39 | private final double minDist; 40 | private final double maxDist; 41 | 42 | public PoissonDiscSampleFunction(Shape shape, double minDist) { 43 | this(shape, minDist, minDist * 2); 44 | } 45 | 46 | public PoissonDiscSampleFunction(Shape shape, double minDist, double maxDist) { 47 | super(shape); 48 | this.minDist = minDist; 49 | this.maxDist = maxDist; 50 | } 51 | 52 | public Vector createSampleNear(Vector v) { 53 | double rr = Math.sqrt(random.nextDouble()); // sqrt to compensate for area change 54 | double radius = minDist + (rr) * (maxDist - minDist); 55 | double angle = 2 * Math.PI * random.nextDouble(); 56 | double x = v.x + radius * Math.cos(angle); 57 | double y = v.y + radius * Math.sin(angle); 58 | return new Vector(x, y); 59 | } 60 | 61 | public double getMimimumDistance(Vector v) { 62 | return minDist; 63 | } 64 | } 65 | 66 | public static abstract class VariablePoissonDiscSampleFunction extends BoundSampleFunction { 67 | public VariablePoissonDiscSampleFunction(Shape shape) { 68 | super(shape); 69 | } 70 | 71 | public Vector createSampleNear(Vector v) { 72 | double minDist = getMimimumDistance(v); 73 | double rr = Math.sqrt(random.nextDouble()); // sqrt to compensate for area change 74 | double radius = minDist + (rr + 1); 75 | double angle = 2 * Math.PI * random.nextDouble(); 76 | double x = v.x + radius * Math.cos(angle); 77 | double y = v.y + radius * Math.sin(angle); 78 | return new Vector(x, y); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/TriangulationLocateTest.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.image.BufferedImage; 6 | import java.io.File; 7 | 8 | import javax.imageio.ImageIO; 9 | 10 | import org.delaunay.Utils; 11 | import org.delaunay.dtfe.painters.PaintTransform; 12 | import org.delaunay.dtfe.painters.TriangulationPainter; 13 | import org.delaunay.dtfe.painters.TriangulationPainterModel; 14 | import org.delaunay.model.Triangle; 15 | import org.delaunay.model.Vertex; 16 | 17 | import com.google.common.base.Function; 18 | 19 | public class TriangulationLocateTest { 20 | 21 | public static void main(String[] args) throws Exception { 22 | int D = 400; 23 | 24 | Triangulation t = new Triangulation(); 25 | t.addAllVertices(Triangulations.randomVertices(200, D, D)); 26 | t.setKeepSuperTriangle(true); 27 | t.triangulate(); 28 | 29 | int right = 0; 30 | int wrong = 0; 31 | 32 | for(final Vertex v : Triangulations.randomVertices(400, D, D)){ 33 | Vertex nearestNaive = Utils.minObject(t.getVertices(), 34 | new Function() { 35 | public Double apply(Vertex vert) { 36 | return vert.subtract(v).lengthSquared(); 37 | } 38 | }); 39 | 40 | Triangle nearestTri = t.locate(v); 41 | Vertex nearestVertex = t.locateNearestVertex(v); 42 | 43 | if (nearestNaive != nearestVertex) { 44 | System.out.println("WRONG"); 45 | System.out.println(v); 46 | System.out.println(nearestNaive); 47 | System.out.println(nearestVertex); 48 | System.out.println(nearestTri != null && nearestTri.getVertices().contains(nearestNaive)); 49 | 50 | System.out.println(); 51 | 52 | 53 | wrong++; 54 | 55 | TriangulationPainter painter = new TriangulationPainter( 56 | new TriangulationPainterModel().setEdgeColor(Color.DARK_GRAY)); 57 | BufferedImage img = painter.paint(t, new PaintTransform(D, D)); 58 | 59 | Graphics2D g = (Graphics2D)img.getGraphics(); 60 | int r = 4; 61 | g.setColor(Color.RED); 62 | g.fillOval((int)(v.x - r / 2), (int)(v.y - r / 2), r, r); 63 | g.setColor(Color.GREEN); 64 | g.fillOval((int)(nearestNaive.x - r / 2), (int)(nearestNaive.y - r / 2), r, r); 65 | if (nearestVertex != null) { 66 | g.setColor(Color.BLUE); 67 | g.fillOval((int) (nearestVertex.x - r / 2), (int) (nearestVertex.y - r / 2), r, r); 68 | } 69 | ImageIO.write(img, "png", new File(String.format("wrongs/wrong_%04d.png", wrong))); 70 | } else{ 71 | right++; 72 | } 73 | } 74 | 75 | System.out.println("Right: " + right + " / " + (right + wrong)); 76 | System.out.println("Wrong: " + wrong + " / " + (right + wrong)); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/DtfeTriangulationMap.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe; 2 | 3 | import org.delaunay.Utils; 4 | import org.delaunay.algorithm.Triangulation.InvalidVertexException; 5 | import org.delaunay.dtfe.interpolation.InterpolationStrategy; 6 | import org.delaunay.model.Triangle; 7 | import org.delaunay.model.TriangulationMap; 8 | import org.delaunay.model.Vector; 9 | import org.delaunay.model.Vertex; 10 | 11 | import com.google.common.base.Function; 12 | 13 | /** 14 | * Computes the Delaunay Tesselation Field Estimator (DTFE): 15 | * http://en.wikipedia.org/wiki/Delaunay_tessellation_field_estimator 16 | * 17 | * This method produces very good heatmaps from discrete data. 18 | * 19 | * @param a model that implements the DensityModel interface 20 | */ 21 | public class DtfeTriangulationMap extends TriangulationMap { 22 | public static enum ScaleType { 23 | LINEAR, LOG; 24 | } 25 | 26 | @Override 27 | public void triangulate() throws InvalidVertexException { 28 | super.triangulate(); 29 | computeDtfe(); 30 | } 31 | 32 | /** 33 | * Returns the density of the vertex. 34 | */ 35 | public double getDensity(Vertex v) { 36 | T model = get(v); 37 | if (model == null) { 38 | return 0.0; 39 | } else if (getTriangulation().neighborsSuperVertex(v)) { 40 | return 0.0; 41 | } else { 42 | return model.getDensity(); 43 | } 44 | } 45 | 46 | public double getInterpolatedDensity(Vector v, InterpolationStrategy strategy) { 47 | return strategy.getDensity(this, v); 48 | } 49 | 50 | private Double maxDensity = null; 51 | 52 | /** 53 | * Returns the maximum density value for all vertices 54 | */ 55 | public double getMaxDensity() { 56 | if (maxDensity == null) { 57 | maxDensity = Utils.maxValue(getTriangulation().getVertices(), 58 | new Function() { 59 | public Double apply(Vertex v) { 60 | return Math.abs(getDensity(v)); 61 | } 62 | }); 63 | } 64 | return maxDensity; 65 | } 66 | 67 | /** 68 | * Returns a value of 0.0 to 1.0, where 1.0 represents the maximum density 69 | * value. This can be plugged directly into a {@link ColorScale} object. 70 | */ 71 | public double getRelativeDensity(double d, ScaleType scaleType) { 72 | if(d == 0) return 0; 73 | boolean neg = (d < 0); 74 | d = Math.abs(d); 75 | double relativeDensity = scaleType == ScaleType.LOG ? 76 | Math.log10(1 + d) / Math.log10(1 + getMaxDensity()) : 77 | d / getMaxDensity(); 78 | return relativeDensity * (neg ? -1 : 1); 79 | } 80 | 81 | private void computeDtfe() { 82 | for (Vertex v : getTriangulation().getVertices()) { 83 | double area = 0.0; 84 | for (Triangle tri : v.getNeighborTriangles()) { 85 | area += tri.getArea(); 86 | } 87 | T model = get(v); 88 | model.setDensity(area == 0 ? 0.0 : model.getWeight() / area); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/org/delaunay/model/Voronoi.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import java.util.Comparator; 4 | import java.util.List; 5 | import java.util.TreeSet; 6 | 7 | import com.google.common.base.Predicate; 8 | import com.google.common.collect.ImmutableList; 9 | import com.google.common.collect.Iterables; 10 | import com.google.common.collect.Lists; 11 | import com.google.common.collect.Sets; 12 | 13 | public class Voronoi extends ConvexPolygon { 14 | public static Voronoi createFromTriangulation(Vertex v) { 15 | return create(v, v.getNeighborVertices(), ImmutableList.copyOf(v.getNeighborTriangles())); 16 | } 17 | 18 | public static Voronoi create( 19 | final Vertex vert, 20 | Iterable neighborVertices, 21 | Iterable neighborTriangles) { 22 | 23 | // Sort neighbors. 24 | Comparator comp = new Comparator() { 25 | public int compare(Vertex o1, Vertex o2) { 26 | return o2.orientation(o1, vert); 27 | } 28 | }; 29 | TreeSet sortedSet = Sets.newTreeSet(comp); 30 | List vertsList = Lists.newArrayList(neighborVertices); 31 | sortedSet.addAll(vertsList); 32 | List sortedNeighbors = Lists.newArrayList(sortedSet); 33 | 34 | // TODO For some reason, sorting a list was not working, so we use a treeset 35 | // Uncomment the following to debug 36 | // Collections.sort(vertsList, comp); 37 | // if(!vertsList.equals(sortedNeighbors)){ 38 | // System.out.println(vertsList); 39 | // System.out.println(sortedNeighbors); 40 | // } 41 | 42 | // Connect circum-centers 43 | List vertices = Lists.newArrayList(); 44 | for (int i = 0; i < sortedNeighbors.size(); i++) { 45 | final Vertex a = i == 0 ? sortedNeighbors.get(sortedNeighbors.size() - 1) : sortedNeighbors.get(i - 1); 46 | final Vertex b = sortedNeighbors.get(i); 47 | Triangle t = Iterables.getFirst(Iterables.filter(neighborTriangles, 48 | new Predicate() { 49 | public boolean apply(Triangle t) { 50 | return t.getVertices().contains(a) && t.getVertices().contains(b); 51 | } 52 | }), null); 53 | if (t == null) { 54 | continue; 55 | /* 56 | * This typically occurs when the cell is outside the convex 57 | * hull of the input vertices or on the border. Note that this 58 | * may be an error, but we do not throw an exception because we 59 | * want to produce a best-effort cell. 60 | */ 61 | } 62 | vertices.add(t.getCircumCenter()); 63 | } 64 | 65 | return new Voronoi(vertices, sortedNeighbors); 66 | } 67 | 68 | private final Iterable neighborVertices; 69 | 70 | private Voronoi(Iterable vertices, Iterable neighborVertices) { 71 | super(vertices); 72 | this.neighborVertices = neighborVertices; 73 | } 74 | 75 | public Iterable getNeighborVertices() { 76 | return neighborVertices; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/org/delaunay/Utils.java: -------------------------------------------------------------------------------- 1 | package org.delaunay; 2 | 3 | import java.awt.geom.Path2D; 4 | import java.awt.geom.Point2D; 5 | import java.util.TreeMap; 6 | 7 | import org.delaunay.model.Vector; 8 | import org.delaunay.model.Vertex; 9 | 10 | import com.google.common.base.Function; 11 | import com.google.common.collect.Iterables; 12 | import com.google.common.collect.Maps; 13 | 14 | public class Utils { 15 | 16 | public static double sum(Iterable values) { 17 | double sum = 0.0; 18 | for (double d : values) { 19 | sum += d; 20 | } 21 | return sum; 22 | } 23 | 24 | public static double maxValue(Iterable values, Function valueFunction) { 25 | TreeMap map = valueMap(values, valueFunction); 26 | if (map.isEmpty()) { 27 | return 0.0; 28 | } 29 | return map.lastKey(); 30 | } 31 | 32 | 33 | public static double minValue(Iterable values, Function valueFunction) { 34 | TreeMap map = valueMap(values, valueFunction); 35 | if (map.isEmpty()) { 36 | return 0.0; 37 | } 38 | return map.firstKey(); 39 | } 40 | 41 | public static T maxObject(Iterable values, Function valueFunction) { 42 | TreeMap map = valueMap(values, valueFunction); 43 | if (map.isEmpty()) { 44 | return null; 45 | } 46 | return map.lastEntry().getValue(); 47 | } 48 | 49 | public static T minObject(Iterable values, Function valueFunction) { 50 | TreeMap map = valueMap(values, valueFunction); 51 | if (map.isEmpty()) { 52 | return null; 53 | } 54 | return map.firstEntry().getValue(); 55 | } 56 | 57 | private static TreeMap valueMap( 58 | Iterable values, 59 | Function valueFunction) { 60 | TreeMap map = Maps.newTreeMap(); 61 | for (T value : values) { 62 | Double d = valueFunction.apply(value); 63 | if (d != null) { 64 | map.put(d, value); 65 | } 66 | } 67 | return map; 68 | } 69 | 70 | public static Iterable toVertices(Iterable vectors) { 71 | return Iterables.transform(vectors, new Function() { 72 | public Vertex apply(Vector vector) { 73 | return new Vertex(vector.x, vector.y); 74 | } 75 | }); 76 | } 77 | 78 | public static Iterable toPoints(Iterable vectors) { 79 | return Iterables.transform(vectors, new Function() { 80 | public Point2D apply(Vector vector) { 81 | return new Point2D.Double(vector.x, vector.y); 82 | } 83 | }); 84 | } 85 | 86 | public static Path2D pathFromPoints(Iterable points) { 87 | Path2D path = new Path2D.Double(); 88 | int i = 0; 89 | for (Point2D point : points) { 90 | if (i++ == 0) { 91 | path.moveTo(point.getX(), point.getY()); 92 | } else { 93 | path.lineTo(point.getX(), point.getY()); 94 | } 95 | } 96 | path.closePath(); 97 | return path; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/samples/SampleBuilder.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm.samples; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.util.List; 5 | import java.util.Random; 6 | 7 | import org.delaunay.algorithm.samples.LocateStrategies.LocateStrategy; 8 | import org.delaunay.algorithm.samples.LocateStrategies.NaiveLocateStrategy; 9 | import org.delaunay.algorithm.samples.SampleFunctions.SampleFunction; 10 | import org.delaunay.model.Vector; 11 | 12 | import com.google.common.collect.Lists; 13 | 14 | public class SampleBuilder { 15 | private final List samples = Lists.newArrayList(); 16 | private int maxTries = 5; 17 | private LocateStrategy locateStrategy = new NaiveLocateStrategy(); 18 | 19 | public int getMaxTries() { 20 | return maxTries; 21 | } 22 | 23 | public SampleBuilder setMaxTries(int maxTries) { 24 | this.maxTries = maxTries; 25 | return this; 26 | } 27 | 28 | public LocateStrategy getLocateStrategy() { 29 | return locateStrategy; 30 | } 31 | 32 | public SampleBuilder setLocateStrategy(LocateStrategy locateStrategy) { 33 | this.locateStrategy = locateStrategy; 34 | return this; 35 | } 36 | 37 | public List getSamples() { 38 | return samples; 39 | } 40 | 41 | public SampleBuilder fill(SampleFunction function) { 42 | List queue = Lists.newArrayList(); 43 | Random random = new Random(System.currentTimeMillis()); 44 | locateStrategy.initialize(samples, function.getBoundingShape().getBounds2D()); 45 | 46 | // Generate and add first sample 47 | Vector firstSample = null; 48 | while (firstSample == null || !locateStrategy.addSample(firstSample)) { 49 | firstSample = function.createSampleIn(function.getBoundingShape()); 50 | } 51 | queue.add(firstSample); 52 | samples.add(firstSample); 53 | 54 | while (!queue.isEmpty()) { 55 | // Get random element from the queue. 56 | int queueIndex = random.nextInt(queue.size()); 57 | Vector sample = queue.get(queueIndex); 58 | 59 | // Attempt to a create new valid sample near the existing sample. 60 | Vector newValidSample = createNewValidSample(function, sample); 61 | 62 | // Add the new valid sample or remove the existing sample from the queue. 63 | if (newValidSample != null && locateStrategy.addSample(newValidSample)) { 64 | queue.add(newValidSample); 65 | samples.add(newValidSample); 66 | } else { 67 | queue.remove(queueIndex); 68 | } 69 | } 70 | return this; 71 | } 72 | 73 | private Vector createNewValidSample(SampleFunction function, Vector sample) { 74 | for (int i = 0; i < maxTries; i++) { 75 | Vector newSample = function.createSampleNear(sample); 76 | if (isValid(function, newSample)) { 77 | return newSample; 78 | } 79 | } 80 | return null; 81 | } 82 | 83 | private boolean isValid(SampleFunction function, final Vector v) { 84 | Vector nearest = locateStrategy.getNearest(v); 85 | if (nearest == null) { 86 | return false; 87 | } 88 | 89 | Rectangle2D bounds = function.getBoundingShape().getBounds2D(); 90 | if (!bounds.contains(v.x, v.y)) { 91 | return false; 92 | } 93 | 94 | double minDist = function.getMimimumDistance(nearest); 95 | return (nearest.subtract(v).lengthSquared() > (minDist * minDist)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/HilbertTableIndex.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm; 2 | 3 | import java.awt.Point; 4 | 5 | 6 | public class HilbertTableIndex { 7 | /** 8 | * @see http://blog.notdot.net/2009/11/Damn-Cool-Algorithms-Spatial-indexing-with-Quadtrees-and-Hilbert-Curves 9 | * 10 | *
 11 | 	 * Coding system applied to the four orientations of the unit shape:
 12 | 	 * 
 13 | 	 * 
 14 | 	 *  square:    0       1       2       3
 15 | 	 *  
 16 | 	 *           1--2    1--0    3--2    3  0
 17 | 	 *  index:   |  |    |          |    |  |
 18 | 	 *           0  3    2--3    0--1    2--1
 19 | 	 *           
 20 | 	 *           0--0    1--3    3--2    2  1
 21 | 	 *  sub-sq:  |  |    |          |    |  |
 22 | 	 *           2  1    1--0    0--2    3--3
 23 | 	 *           
 24 | 	 *  char:      A       C       D       U
 25 | 	 *           
 26 | 	 *           A--A    C--U    U--D    D  C
 27 | 	 *  sub-sq:  |  |    |          |    |  |
 28 | 	 *           D  C    C--A    A--D    U--U
 29 | 	 * 
 30 | 	 * 
31 | */ 32 | 33 | private static final int[][] squares = new int[4][]; 34 | private static final int[][] indexes = new int[4][]; 35 | private static final int[][] reverse = new int[4][]; 36 | private static final int[][] revsqrs = new int[4][]; 37 | static{ 38 | // using i = xb<<1 | yb 39 | squares[0] = new int[]{2,0,1,0}; 40 | squares[1] = new int[]{1,1,0,3}; 41 | squares[2] = new int[]{0,3,2,2}; 42 | squares[3] = new int[]{3,2,3,1}; 43 | 44 | indexes[0] = new int[]{0,1,3,2}; 45 | indexes[1] = new int[]{2,1,3,0}; 46 | indexes[2] = new int[]{0,3,1,2}; 47 | indexes[3] = new int[]{2,3,1,0}; 48 | 49 | reverse[0] = new int[]{0,1,3,2}; 50 | reverse[1] = new int[]{3,1,0,2}; 51 | reverse[2] = new int[]{0,2,3,1}; 52 | reverse[3] = new int[]{3,2,0,1}; 53 | 54 | revsqrs[0] = new int[]{2,0,0,1}; 55 | revsqrs[1] = new int[]{3,1,1,0}; 56 | revsqrs[2] = new int[]{0,2,2,3}; 57 | revsqrs[3] = new int[]{1,3,3,2}; 58 | } 59 | 60 | private final int order; 61 | private final int startsquare; 62 | 63 | public HilbertTableIndex(final int order) { 64 | this(order,0); 65 | } 66 | 67 | public HilbertTableIndex(final int order, final int startsquare) { 68 | if(order <= 0) throw new IllegalArgumentException("order must be > 0"); 69 | this.order = order; 70 | this.startsquare = startsquare; 71 | } 72 | 73 | public int getIndex(Point p) { 74 | final int x = p.x; 75 | final int y = p.y; 76 | if (x < 0 || x >= 1 << order || y < 0 || y >= 1 << order) return -1; 77 | 78 | int index = 0; 79 | int sq = startsquare; 80 | int o = order - 1; 81 | while (o >= 0) { 82 | int i = ((((x >> o) & 1) << 1) | ((y >> o) & 1)); 83 | index = (index << 2) | indexes[sq][i]; 84 | sq = squares[sq][i]; 85 | o--; 86 | } 87 | 88 | return index; 89 | } 90 | 91 | public Point getPoint(int index) { 92 | if (index < 0 || index > (1 << (2*order))) return null; 93 | 94 | int x = 0; 95 | int y = 0; 96 | int sq = startsquare; 97 | int o = order - 1; 98 | while (o >= 0) { 99 | int i = (index >> (o * 2) & 3); 100 | x = x << 1 | (reverse[sq][i] >> 1 & 1); 101 | y = y << 1 | (reverse[sq][i] & 1); 102 | sq = revsqrs[sq][i]; 103 | o--; 104 | } 105 | return new Point(x, y); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/ColorScales.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe; 2 | 3 | import java.awt.Color; 4 | import java.util.List; 5 | 6 | import com.google.common.collect.Lists; 7 | 8 | public class ColorScales { 9 | 10 | public static final ColorScale MODIFIED_RAINBOW = new ColorScale() 11 | .stopAlpha(Color.HSBtoRGB(0.0f, 0.6f, 1.0f), 0x00, 0.1) 12 | .stopAlpha(Color.HSBtoRGB(1.0f, 0.8f, 1.0f), 0x20, 0.4) 13 | .stopAlpha(Color.HSBtoRGB(0.8f, 0.9f, 1.0f), 0x40, 0.5) 14 | .stopAlpha(Color.HSBtoRGB(0.6f, 1.0f, 1.0f), 0x60, 0.6) 15 | .stopAlpha(Color.HSBtoRGB(0.4f, 1.0f, 1.0f), 0x80, 0.7) 16 | .stopAlpha(Color.HSBtoRGB(0.2f, 1.0f, 1.0f), 0xA0, 0.8) 17 | .stopAlpha(0xFF0000, 0xB0, 0.801) 18 | .stopAlpha(0xFF00FF, 0xB0, 1.0); 19 | 20 | public static final ColorScale LINEAR_RAINBOW = new ColorScale() 21 | .stopAlpha(Color.HSBtoRGB(0.0f, 1.0f, 1.0f), 0x00, 0.0) 22 | .stopAlpha(Color.HSBtoRGB(1.0f, 1.0f, 1.0f), 0x80, 0.2) 23 | .stopAlpha(Color.HSBtoRGB(0.8f, 1.0f, 1.0f), 0xFF, 0.4) 24 | .stopAlpha(Color.HSBtoRGB(0.6f, 1.0f, 1.0f), 0xFF, 0.6) 25 | .stopAlpha(Color.HSBtoRGB(0.4f, 1.0f, 1.0f), 0xFF, 0.8) 26 | .stopAlpha(Color.HSBtoRGB(0.2f, 1.0f, 1.0f), 0xFF, 1.0); 27 | 28 | public static final ColorScale LINEAR_RAINBOW_NO_ALPHA = new ColorScale() 29 | .stopAlpha(Color.HSBtoRGB(0.0f, 1.0f, 1.0f), 0xFF, 0.0) 30 | .stopAlpha(Color.HSBtoRGB(1.0f, 1.0f, 1.0f), 0xFF, 0.2) 31 | .stopAlpha(Color.HSBtoRGB(0.8f, 1.0f, 1.0f), 0xFF, 0.4) 32 | .stopAlpha(Color.HSBtoRGB(0.6f, 1.0f, 1.0f), 0xFF, 0.6) 33 | .stopAlpha(Color.HSBtoRGB(0.4f, 1.0f, 1.0f), 0xFF, 0.8) 34 | .stopAlpha(Color.HSBtoRGB(0.2f, 1.0f, 1.0f), 0xFF, 1.0); 35 | 36 | // http://en.wikipedia.org/wiki/Color_temperature 37 | public static final ColorScale TEMPURATURE = new ColorScale() 38 | .stopAlpha(0xFF3C00, 0x00, 0.0) 39 | .stopAlpha(0xFF7400, 0x40, 0.2) 40 | .stopAlpha(0xFFAE2E, 0x80, 0.5) 41 | .stopAlpha(0xC3C0C2, 0xE0, 0.8) 42 | .stopAlpha(0x96B1EB, 0xFF, 0.9) 43 | .stopAlpha(0x779EFF, 0xFF, 1.0); 44 | 45 | public static final ColorScale BLUE_TO_YELLOW = new ColorScale() 46 | .stopAlpha(0x0000FF, 0x00, 0.0) 47 | .stopAlpha(0x0000FF, 0x80, 0.2) 48 | .stopAlpha(0xFFFF00, 0xFF, 1.0); 49 | 50 | public static final ColorScale PURPLE_TO_GREEN = new ColorScale() 51 | .stopAlpha(0x260404, 0x00, 0.2) 52 | .stopAlpha(0x260404, 0x80, 0.4) 53 | .stopAlpha(0x6B3BAB, 0xC0, 0.6) 54 | .stopAlpha(0x4CFFB6, 0xFF, 0.9); 55 | 56 | public static final ColorScale PURPLE_TO_GREEN_LINEAR = new ColorScale() 57 | .stopAlpha(0x260404, 0x00, 0.0) 58 | .stopAlpha(0x260404, 0x80, 0.2) 59 | .stopAlpha(0x6B3BAB, 0xC0, 0.4) 60 | .stopAlpha(0x4CFFB6, 0xFF, 1.0); 61 | 62 | public static final ColorScale RED_TO_BLUE_POS_NEG = new ColorScale() 63 | .stopAlpha(0xFF0000, 0xFF, -1.0) 64 | .stopAlpha(0xFFFF00, 0x60, -0.5) 65 | .stopAlpha(0xFFFFFF, 0x00, 0.0) 66 | .stopAlpha(0x00FFFF, 0x60, 0.5) 67 | .stopAlpha(0x0000FF, 0xFF, 1.0); 68 | 69 | public static final ColorScale RED_TO_BLUE = new ColorScale() 70 | .stopAlpha(0xFF0000, 0xFF, 0.0) 71 | .stopAlpha(0xFFFF00, 0x60, 0.25) 72 | .stopAlpha(0xFFFFFF, 0x00, 0.5) 73 | .stopAlpha(0x00FFFF, 0x60, 0.75) 74 | .stopAlpha(0x0000FF, 0xFF, 1.0); 75 | 76 | public static List getDefaultColorScales() { 77 | return Lists.newArrayList( 78 | MODIFIED_RAINBOW, 79 | LINEAR_RAINBOW, 80 | TEMPURATURE, 81 | PURPLE_TO_GREEN, 82 | RED_TO_BLUE_POS_NEG, 83 | BLUE_TO_YELLOW); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/org/delaunay/model/Triangle.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.model; 2 | 3 | import java.util.LinkedHashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import com.google.common.collect.Lists; 8 | import com.google.common.collect.Sets; 9 | 10 | public strictfp class Triangle { 11 | 12 | public final Vertex a, b, c; 13 | private final Vector circumCenter; 14 | private final double rSquared; 15 | private final LinkedHashSet vertices; 16 | private final List edges; 17 | 18 | public Triangle(Vertex v_a, Vertex v_b, Vertex v_c) { 19 | // Enforce winding rule 20 | boolean swap = v_c.orientation(v_a, v_b) > 0; 21 | 22 | this.a = v_a; 23 | this.b = swap ? v_c : v_b; 24 | this.c = swap ? v_b : v_c; 25 | 26 | this.vertices = Sets.newLinkedHashSet(Lists.newArrayList(a, b, c)); 27 | this.edges = Lists.newArrayList( 28 | new Edge(a, b), 29 | new Edge(c, a), 30 | new Edge(b, c)); 31 | 32 | // compute circum center 33 | // http://www.ics.uci.edu/~eppstein/junkyard/circumcenter.html 34 | double d = ((a.x - c.x) * (b.y - c.y) - (b.x - c.x) * (a.y - c.y)) * 2; 35 | double c_x = (((a.x - c.x) * (a.x + c.x) + (a.y - c.y) * (a.y + c.y)) * (b.y - c.y) - 36 | ((b.x - c.x) * (b.x + c.x) + (b.y - c.y) * (b.y + c.y)) * (a.y - c.y)) 37 | / d; 38 | double c_y = (((b.x - c.x) * (b.x + c.x) + (b.y - c.y) * (b.y + c.y)) * (a.x - c.x) - 39 | ((a.x - c.x) * (a.x + c.x) + (a.y - c.y) * (a.y + c.y)) * (b.x - c.x)) 40 | / d; 41 | this.circumCenter = new Vector(c_x, c_y); 42 | this.rSquared = (c.x - c_x) * (c.x - c_x) + (c.y - c_y) * (c.y - c_y); 43 | } 44 | 45 | public LinkedHashSet getVertices() { 46 | return vertices; 47 | } 48 | 49 | public Vector getCircumCenter() { 50 | return circumCenter; 51 | } 52 | 53 | public boolean isInCircum(Vector v){ 54 | v = v.subtract(circumCenter); 55 | return v.lengthSquared() <= rSquared; 56 | } 57 | 58 | public Triangle nextWalk(Vector v) { 59 | if (v.orientation(b, c) > 0) { 60 | validateOpposites(); 61 | return oppositeBC; 62 | } else if (v.orientation(c, a) > 0) { 63 | validateOpposites(); 64 | return oppositeCA; 65 | } else if (v.orientation(a, b) > 0) { 66 | validateOpposites(); 67 | return oppositeAB; 68 | } 69 | return this; 70 | } 71 | 72 | public Triangle opposite(Vertex a, Vertex b) { 73 | for (Triangle t : a.getNeighborTriangles()) { 74 | if (t != this && t.vertices.contains(b)) { 75 | return t; 76 | } 77 | } 78 | return null; 79 | } 80 | 81 | // Cache opposites for performance 82 | private Triangle oppositeAB = null; 83 | private Triangle oppositeBC = null; 84 | private Triangle oppositeCA = null; 85 | private boolean oppositesValid = false; 86 | 87 | private void validateOpposites() { 88 | if (oppositesValid) { 89 | return; 90 | } 91 | oppositeAB = opposite(a, b); 92 | oppositeBC = opposite(b, c); 93 | oppositeCA = opposite(c, a); 94 | oppositesValid = true; 95 | } 96 | 97 | public void invalidateNeighbors() { 98 | oppositesValid = false; 99 | neighborsValid = false; 100 | } 101 | 102 | private Set neighbors = null; 103 | private boolean neighborsValid = false; 104 | 105 | public Set getNeighbors() { 106 | if (neighborsValid) { 107 | return neighbors; 108 | } 109 | neighbors = Sets.newHashSet(); 110 | neighbors.addAll(a.getNeighborTriangles()); 111 | neighbors.addAll(b.getNeighborTriangles()); 112 | neighbors.addAll(c.getNeighborTriangles()); 113 | neighbors.remove(this); 114 | neighborsValid = true; 115 | return neighbors; 116 | } 117 | 118 | public Iterable getEdges() { 119 | return edges; 120 | } 121 | 122 | private Double area = null; 123 | 124 | public double getArea() { 125 | if (area == null) { 126 | area = (c.subtract(a)).cross((b.subtract(a))) / 2.0; 127 | } 128 | return area; 129 | } 130 | 131 | @Override 132 | public String toString() { 133 | return String.format("%s -> %s -> %s", a, b, c); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/org/delaunay/TriangulationDemo.java: -------------------------------------------------------------------------------- 1 | package org.delaunay; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import javax.imageio.ImageIO; 10 | 11 | import org.delaunay.algorithm.Triangulation; 12 | import org.delaunay.algorithm.Triangulation.DebugLogger; 13 | import org.delaunay.algorithm.Triangulation.InvalidVertexException; 14 | import org.delaunay.algorithm.Triangulations; 15 | import org.delaunay.dtfe.BasicDensityModel; 16 | import org.delaunay.dtfe.ColorScales; 17 | import org.delaunay.dtfe.DensityModel; 18 | import org.delaunay.dtfe.DtfeTriangulationMap; 19 | import org.delaunay.dtfe.interpolation.InterpolationStrategies; 20 | import org.delaunay.dtfe.painters.DtfePainter; 21 | import org.delaunay.dtfe.painters.DtfePainterModel; 22 | import org.delaunay.dtfe.painters.PaintTransform; 23 | import org.delaunay.dtfe.painters.TriangulationPainter; 24 | import org.delaunay.dtfe.painters.TriangulationPainterModel; 25 | import org.delaunay.model.Vertex; 26 | 27 | /** 28 | * This class demonstrates various ways of creating triangulations and DTFEs, 29 | * and how to generate images from them. 30 | */ 31 | public class TriangulationDemo { 32 | private static final int WIDTH = 800; 33 | private static final int HEIGHT = 800; 34 | 35 | public static void drawTriangulation(Triangulation t, int w, int h, String filename) 36 | throws IOException { 37 | 38 | TriangulationPainter painter = new TriangulationPainter(new TriangulationPainterModel() 39 | .setEdgeColor(new Color(0x2222AA)) 40 | .setEdgeStrokeWidth(1.5f)); 41 | 42 | BufferedImage img = painter.paint(t, new PaintTransform(w, h)); 43 | ImageIO.write(img, "png", new File(filename)); 44 | } 45 | 46 | public static void drawDtfe(DtfeTriangulationMap dtfe, int w, int h, String filename) 47 | throws IOException { 48 | 49 | DtfePainter painter = new DtfePainter(new DtfePainterModel() 50 | .setInterpolationStrategy(InterpolationStrategies.createNaturalNeighbor()) 51 | .setDensityScalar(50) 52 | .setEdgeColor(new Color(0x10000000, true)) 53 | .setColorScale(ColorScales.PURPLE_TO_GREEN_LINEAR)); 54 | 55 | BufferedImage img = painter.paint(dtfe, new PaintTransform(w, h)); 56 | ImageIO.write(img, "png", new File(filename)); 57 | } 58 | 59 | public static void fourLiner() throws Exception { 60 | Triangulation t = new Triangulation(); 61 | t.addAllVertices(Triangulations.randomVertices(1000, 400, 400)); 62 | t.triangulate(); 63 | TriangulationDemo.drawTriangulation(t, 400, 400, "triangulation.png"); 64 | } 65 | 66 | public static void createTriangulationAndDtfeDemo() throws InvalidVertexException, IOException { 67 | // Generate vertices 68 | 69 | // Triangulate 70 | long start = System.nanoTime(); 71 | Triangulation t = new Triangulation(); 72 | t.addAllVertices(Triangulations.randomVertices(10000, WIDTH, HEIGHT)); 73 | t.setDebugLogger(new DebugLogger() { 74 | public void debug(String str) { 75 | System.out.println(str); 76 | } 77 | }); 78 | t.triangulate(); 79 | System.out.println(String.format("Time to triangulate %,d vertices: %d msec.", 10000, 80 | TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS))); 81 | System.out.println(String.format("Average hops per locate: %.2f", (float)t.getHopCount() / t.getLocateCount())); 82 | 83 | // DTFE 84 | start = System.nanoTime(); 85 | DtfeTriangulationMap dtfe = new DtfeTriangulationMap(); 86 | for (Vertex v : Triangulations.randomGaussian(1000, WIDTH, HEIGHT)) { 87 | dtfe.put(v.x, v.y, new BasicDensityModel()); 88 | } 89 | dtfe.triangulate(); 90 | System.out.println(String.format("Time to create DTFE: %d msec.", 91 | TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS))); 92 | 93 | // Draw Results 94 | System.out.println("Creating images"); 95 | drawTriangulation(t, WIDTH, HEIGHT, "triangulation.png"); 96 | start = System.nanoTime(); 97 | drawDtfe(dtfe, WIDTH, HEIGHT, "dtfe.png"); 98 | System.out.println(String.format("Time to draw DTFE: %d msec.", 99 | TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS))); 100 | System.out.println("Done"); 101 | } 102 | 103 | public static void main(String[] args) throws Exception { 104 | //threeLiner(); 105 | createTriangulationAndDtfeDemo(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/org/delaunay/dtfe/IsolineBuilder.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.dtfe; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import org.delaunay.dtfe.DtfeTriangulationMap.ScaleType; 8 | import org.delaunay.model.Edge; 9 | import org.delaunay.model.Triangle; 10 | import org.delaunay.model.Vector; 11 | import org.delaunay.model.Vertex; 12 | 13 | import com.google.common.collect.ArrayListMultimap; 14 | import com.google.common.collect.Iterables; 15 | import com.google.common.collect.Lists; 16 | import com.google.common.collect.Multimap; 17 | import com.google.common.collect.Sets; 18 | 19 | public class IsolineBuilder { 20 | 21 | private final DtfeTriangulationMap dtfe; 22 | private ScaleType scaleType = ScaleType.LOG; 23 | 24 | public IsolineBuilder(DtfeTriangulationMap dtfe) { 25 | this.dtfe = dtfe; 26 | } 27 | 28 | public IsolineBuilder setScaleType(ScaleType scaleType) { 29 | this.scaleType = scaleType; 30 | return this; 31 | } 32 | 33 | /** 34 | * Returns a list of list of vectors. Each list of vectors is a continuous 35 | * isoline at the specified value. 36 | */ 37 | public List> getIsoLines(double value) { 38 | Set segments = Sets.newLinkedHashSet(); 39 | Multimap segmentsMap = ArrayListMultimap.create(); 40 | 41 | // Compute segments from triangles that pass through value 42 | for (Triangle tri : dtfe.getTriangulation().getTriangles()) { 43 | // Compare vertex density with iso value 44 | List above = Lists.newArrayList(); 45 | List below = Lists.newArrayList(); 46 | for (Vertex v : tri.getVertices()) { 47 | if (dtfe.getRelativeDensity(dtfe.getDensity(v), scaleType) > value) { 48 | above.add(v); 49 | } else { 50 | below.add(v); 51 | } 52 | } 53 | 54 | // All vertices above or below value 55 | if (above.size() == 3 || below.size() == 3 || above.size() + below.size() != 3) { 56 | continue; 57 | } 58 | // One vertex above value (2 below) 59 | else if (above.size() == 1) { 60 | Segment segment = new Segment(Iterables.getOnlyElement(above), below.get(0), below.get(1), value); 61 | segmentsMap.put(segment.e0, segment); 62 | segmentsMap.put(segment.e1, segment); 63 | segments.add(segment); 64 | } 65 | // One vertex below value (2 above) 66 | else if (below.size() == 1) { 67 | Segment segment = new Segment(Iterables.getOnlyElement(below), above.get(0), above.get(1), value); 68 | segmentsMap.put(segment.e0, segment); 69 | segmentsMap.put(segment.e1, segment); 70 | segments.add(segment); 71 | } 72 | } 73 | 74 | // Connect line segments into lists of vectors 75 | List> paths = Lists.newArrayList(); 76 | while (!segments.isEmpty()) { 77 | // Poll any segment 78 | Segment segment = Iterables.getFirst(segments, null); 79 | segments.remove(segment); 80 | 81 | Edge startEdge = segment.e0; 82 | Edge headEdge = segment.e1; 83 | List vectors = Lists.newArrayList(segment.v0, segment.v1); 84 | 85 | while (segment != null) { 86 | // Find other segment that touches the same edge 87 | Collection segs = segmentsMap.get(headEdge); 88 | segs.remove(segment); 89 | Segment nextSegment = Iterables.getFirst(segs, null); 90 | 91 | if (nextSegment == null) { 92 | // TODO log error 93 | break; 94 | } 95 | 96 | segments.remove(nextSegment); 97 | 98 | // Connect the segment (switch direction of segment if necessary) 99 | if (headEdge.equals(nextSegment.e0)) { 100 | vectors.add(nextSegment.v1); 101 | headEdge = nextSegment.e1; 102 | } else { 103 | vectors.add(nextSegment.v0); 104 | headEdge = nextSegment.e0; 105 | } 106 | segment = nextSegment; 107 | 108 | // If we reach the beginning, this path is complete 109 | if (headEdge.equals(startEdge)) { 110 | paths.add(vectors); 111 | break; 112 | } 113 | } 114 | } 115 | return paths; 116 | } 117 | 118 | private class Segment { 119 | final Vector v0; 120 | final Edge e0; 121 | final Vector v1; 122 | final Edge e1; 123 | 124 | private Segment(Vertex vCommon, Vertex v0, Vertex v1, double value) { 125 | // Compute the location of the segment endpoints based on the 126 | // density of the vertices and the iso value 127 | double dCommon = dtfe.getRelativeDensity(dtfe.getDensity(vCommon), scaleType); 128 | double d0 = dtfe.getRelativeDensity(dtfe.getDensity(v0), scaleType); 129 | double d1 = dtfe.getRelativeDensity(dtfe.getDensity(v1), scaleType); 130 | double t0 = (value - dCommon) / (d0 - dCommon); 131 | double t1 = (value - dCommon) / (d1 - dCommon); 132 | 133 | this.e0 = new Edge(vCommon, v0); 134 | this.v0 = v0.subtract(vCommon).multiply(t0).add(vCommon); 135 | this.e1 = new Edge(vCommon, v1); 136 | this.v1 = v1.subtract(vCommon).multiply(t1).add(vCommon); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/org/delaunay/ExampleImagesDemo.java: -------------------------------------------------------------------------------- 1 | package org.delaunay; 2 | 3 | import java.awt.Color; 4 | import java.awt.Rectangle; 5 | import java.awt.image.BufferedImage; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.Random; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import javax.imageio.ImageIO; 12 | 13 | import org.delaunay.algorithm.Triangulations; 14 | import org.delaunay.dtfe.BasicDensityModel; 15 | import org.delaunay.dtfe.ColorScale; 16 | import org.delaunay.dtfe.ColorScales; 17 | import org.delaunay.dtfe.DtfeTriangulationMap; 18 | import org.delaunay.dtfe.Dtfes; 19 | import org.delaunay.dtfe.Dtfes.DifferenceDensityModel; 20 | import org.delaunay.dtfe.interpolation.InterpolationStrategies; 21 | import org.delaunay.dtfe.painters.ColorScaleLegendPainter; 22 | import org.delaunay.dtfe.painters.ColorScaleLegendPainter.ColorScalePainterModel; 23 | import org.delaunay.dtfe.painters.DtfePainter; 24 | import org.delaunay.dtfe.painters.DtfePainterModel; 25 | import org.delaunay.dtfe.painters.PaintTransform; 26 | import org.delaunay.dtfe.painters.TriangulationPainter; 27 | import org.delaunay.dtfe.painters.TriangulationPainterModel; 28 | import org.delaunay.model.Vertex; 29 | 30 | /** 31 | * This class generates the images that are used in the wiki gallery on github. 32 | */ 33 | @SuppressWarnings("unused") 34 | public class ExampleImagesDemo { 35 | private static final int WIDTH = 800; 36 | private static final int HEIGHT = 800; 37 | 38 | public static void createInterpolationExamples() throws Exception { 39 | // Generate random distribution of points 40 | DtfeTriangulationMap dtfe = new DtfeTriangulationMap(); 41 | for (Vertex v : Triangulations.randomGaussian(1000, WIDTH, HEIGHT)) { 42 | dtfe.put(v.x, v.y, new BasicDensityModel()); 43 | } 44 | dtfe.triangulate(); 45 | 46 | // Make sure we have dirs 47 | new File("examples").mkdirs(); 48 | 49 | // Draw overall triangulation 50 | long start = System.nanoTime(); 51 | BufferedImage img = new TriangulationPainter(new TriangulationPainterModel() 52 | .setEdgeColor(Color.BLACK) 53 | .setEdgeStrokeWidth(1.5f)).paint( 54 | dtfe.getTriangulation(), 55 | new PaintTransform(WIDTH, HEIGHT)); 56 | System.out.println(String.format("examples/dtfe_triangulation_full.png: %d msec.", TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS))); 57 | ImageIO.write(img, "png", new File("examples/dtfe_triangulation_full.png")); 58 | 59 | // Draw individual interpolations 60 | DtfePainterModel model = new DtfePainterModel() 61 | .setDensityScalar(35) 62 | .setColorScale(ColorScales.PURPLE_TO_GREEN_LINEAR); 63 | model.setInterpolationStrategy(InterpolationStrategies.createMean()); 64 | createSampleImages(dtfe, model, "mean"); 65 | model.setInterpolationStrategy(InterpolationStrategies.createNearestNeighbor()); 66 | createSampleImages(dtfe, model, "near_neighbor"); 67 | model.setInterpolationStrategy(InterpolationStrategies.createBarycentricLinear()); 68 | createSampleImages(dtfe, model, "linear"); 69 | model.setInterpolationStrategy(InterpolationStrategies.createNaturalNeighbor()); 70 | createSampleImages(dtfe, model, "natural_neighbor"); 71 | } 72 | 73 | private static void createSampleImages(DtfeTriangulationMap dtfe, 74 | DtfePainterModel model, String name) throws IOException { 75 | Rectangle fullRect = new Rectangle(0, 0, WIDTH, HEIGHT); 76 | Rectangle zoomRect = new Rectangle(WIDTH / 2 + 50, HEIGHT / 2 - 100, 200, 200); 77 | 78 | long start = System.nanoTime(); 79 | String file0 = "examples/dtfe_interp_" + name + "_full.png"; 80 | ImageIO.write(new DtfePainter(model).paint(dtfe, new PaintTransform(WIDTH, HEIGHT, fullRect)), 81 | "png", new File(file0)); 82 | System.out.println(String.format("%s: %d msec.", file0, TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS))); 83 | 84 | start = System.nanoTime(); 85 | String file1 = "examples/dtfe_interp_" + name + "_zoom.png"; 86 | ImageIO.write(new DtfePainter(model).paint(dtfe, new PaintTransform(WIDTH, HEIGHT, zoomRect)), 87 | "png", new File(file1)); 88 | System.out.println(String.format("%s: %d msec.", file1, TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS))); 89 | } 90 | 91 | private static void createPosNegExample() throws Exception { 92 | // Generate random distribution of points with positive and negative weights 93 | DtfeTriangulationMap dtfe = new DtfeTriangulationMap(); 94 | Random random = new Random(System.currentTimeMillis()); 95 | for (Vertex v : Triangulations.randomGaussian(1000, WIDTH, HEIGHT)) { 96 | dtfe.put(v.x, v.y, new BasicDensityModel(random.nextDouble() * (random.nextBoolean() ? 1 : -1))); 97 | } 98 | dtfe.triangulate(); 99 | 100 | // Paint 101 | DtfePainterModel model = new DtfePainterModel() 102 | .setDensityScalar(35) 103 | .setColorScale(ColorScales.RED_TO_BLUE_POS_NEG); 104 | model.setInterpolationStrategy(InterpolationStrategies.createBarycentricLinear()); 105 | createSampleImages(dtfe, model, "posneg"); 106 | } 107 | 108 | private static void createDiffExample() throws Exception { 109 | // Generate random distribution of points 110 | DtfeTriangulationMap dtfeA = new DtfeTriangulationMap(); 111 | DtfeTriangulationMap dtfeB = new DtfeTriangulationMap(); 112 | Random random = new Random(System.currentTimeMillis()); 113 | for (Vertex v : Triangulations.randomGaussian(1000, WIDTH, HEIGHT)) { 114 | dtfeA.put(v.x - WIDTH / 32, v.y - HEIGHT / 32, new BasicDensityModel(0.5 + random.nextDouble())); 115 | } 116 | for (Vertex v : Triangulations.randomGaussian(1000, WIDTH, HEIGHT)) { 117 | dtfeB.put(v.x + WIDTH / 32, v.y + HEIGHT / 32, new BasicDensityModel(0.5 + random.nextDouble())); 118 | } 119 | dtfeA.triangulate(); 120 | dtfeB.triangulate(); 121 | 122 | // Paint individual dtfes 123 | DtfePainterModel modelAB = new DtfePainterModel() 124 | .setDensityScalar(25) 125 | .setColorScale(ColorScales.PURPLE_TO_GREEN_LINEAR) 126 | .setInterpolationStrategy(InterpolationStrategies.createBarycentricLinear()); 127 | createSampleImages(dtfeA, modelAB, "diff_a"); 128 | createSampleImages(dtfeB, modelAB, "diff_b"); 129 | 130 | // Create difference 131 | DtfeTriangulationMap> diff = Dtfes.difference( 132 | dtfeA, dtfeB, InterpolationStrategies.createBarycentricLinear()); 133 | 134 | // Paint diff dtfe 135 | createSampleImages(diff, new DtfePainterModel() 136 | .setDensityScalar(25) 137 | .setColorScale(ColorScales.RED_TO_BLUE_POS_NEG) 138 | .setInterpolationStrategy(InterpolationStrategies.createBarycentricLinear()), "diff"); 139 | } 140 | 141 | private static void createScaleExamples() throws Exception { 142 | ColorScaleLegendPainter painter = new ColorScaleLegendPainter(new ColorScalePainterModel()); 143 | paintScale(painter, ColorScales.LINEAR_RAINBOW, "rainbow"); 144 | paintScale(painter, ColorScales.PURPLE_TO_GREEN_LINEAR, "purple_green"); 145 | paintScale(painter, ColorScales.TEMPURATURE, "tempurature"); 146 | paintScale(painter, ColorScales.RED_TO_BLUE_POS_NEG, "red_blue_pos_neg"); 147 | paintScale(painter, ColorScales.BLUE_TO_YELLOW, "blue_yellow"); 148 | } 149 | 150 | private static void paintScale( 151 | ColorScaleLegendPainter painter, 152 | ColorScale scale, 153 | String name) throws IOException { 154 | String path = "examples/colorscale_" + name + ".png"; 155 | ImageIO.write(painter.createLegendImage(scale), "png", new File(path)); 156 | } 157 | 158 | public static void main(String[] args) throws Exception { 159 | //createInterpolationExamples(); 160 | //createPosNegExample(); 161 | //createDiffExample(); 162 | createScaleExamples(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/org/delaunay/algorithm/Triangulation.java: -------------------------------------------------------------------------------- 1 | package org.delaunay.algorithm; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.Comparator; 7 | import java.util.LinkedHashSet; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | import org.delaunay.model.Edge; 12 | import org.delaunay.model.Triangle; 13 | import org.delaunay.model.Vector; 14 | import org.delaunay.model.Vectors; 15 | import org.delaunay.model.Vertex; 16 | 17 | import com.google.common.base.Function; 18 | import com.google.common.base.Predicate; 19 | import com.google.common.collect.Iterables; 20 | import com.google.common.collect.Lists; 21 | import com.google.common.collect.Sets; 22 | 23 | /** 24 | * A fast Delaunay Triangulation implementation. 25 | */ 26 | @SuppressWarnings("serial") 27 | public strictfp class Triangulation { 28 | 29 | public static class NonDelaunayException extends RuntimeException { 30 | } 31 | 32 | public static class InvalidVertexException extends Exception { 33 | } 34 | 35 | public static interface DebugLogger{ 36 | public void debug(String str); 37 | } 38 | 39 | public static enum VertexExceptionStrategy{ 40 | THROW_EXCEPTION, 41 | CATCH_AND_DROP_VERTEX, 42 | ; 43 | } 44 | 45 | private Vertex[] superVerts = new Vertex[]{}; 46 | private LinkedHashSet triangles = Sets.newLinkedHashSet(); 47 | private LinkedHashSet inputVertices = Sets.newLinkedHashSet(); 48 | private LinkedHashSet vertices = Sets.newLinkedHashSet(); 49 | private Triangle lastLocatedTriangle = null; 50 | private int hopCount = 0; 51 | private int locateCount = 0; 52 | private boolean keepSuperTriangle = false; 53 | private VertexExceptionStrategy vertexExceptionStrategy = VertexExceptionStrategy.THROW_EXCEPTION; 54 | 55 | /* 56 | * The hilbert order determines the granularity of the hilbert curve. For 57 | * example, a value of 16 produces a square with with length and width 2^16, 58 | * resulting in 2^16 * 2^16 = 2^32 cells. This is typically good enough for 59 | * a triangulation up to 4 Billion vertices. Running time for coordinate 60 | * conversion would be O(16). 61 | */ 62 | private int hilbertOrder = 16; 63 | 64 | /* 65 | * Determines the scale of the super triangle. Increase this number if you 66 | * need to vertex locates from farther out from the bounding box of the 67 | * vertices. 68 | */ 69 | private double superTriangleScale = 2.0; 70 | 71 | private DebugLogger log = new DebugLogger() { 72 | public void debug(String str) { 73 | // null implementation 74 | } 75 | }; 76 | 77 | public int getHopCount() { 78 | return hopCount; 79 | } 80 | 81 | public int getLocateCount() { 82 | return locateCount; 83 | } 84 | 85 | public void setDebugLogger(DebugLogger log) { 86 | this.log = log; 87 | } 88 | 89 | public Vertex addVertex(double x, double y) { 90 | Vertex vertex = new Vertex(x, y); 91 | inputVertices.add(vertex); 92 | return vertex; 93 | } 94 | 95 | public void addVertex(Vertex v) { 96 | inputVertices.add(v); 97 | } 98 | 99 | public void addAllVertices(Iterable vs) { 100 | Iterables.addAll(inputVertices, vs); 101 | } 102 | 103 | public LinkedHashSet getInputVertices() { 104 | return inputVertices; 105 | } 106 | 107 | public LinkedHashSet getVertices() { 108 | return vertices; 109 | } 110 | 111 | public List getVerticesInBounds(final Rectangle2D rect) { 112 | return Lists.newArrayList(Iterables.filter(getVertices(), new Predicate() { 113 | public boolean apply(Vertex v) { 114 | return rect.contains(v.toPoint()); 115 | } 116 | })); 117 | } 118 | 119 | public LinkedHashSet getTriangles() { 120 | return triangles; 121 | } 122 | 123 | /** 124 | * If set to true, the supertriangle will not be removed at the end of the 125 | * triangulation method. This allows points outside the convex hull of 126 | * vertices to be located. 127 | */ 128 | public void setKeepSuperTriangle(boolean keepSuperTriangle) { 129 | this.keepSuperTriangle = keepSuperTriangle; 130 | } 131 | 132 | public Vertex locateNearestVertex(Vector v) { 133 | Triangle located = locate(v); 134 | if (located == null) { 135 | return null; 136 | } 137 | 138 | Vertex bestVertex = null; 139 | double dist = Double.MAX_VALUE; 140 | 141 | for (Triangle tri : getCircumcircleTriangles(v, located)) { 142 | for (Vertex vert : tri.getVertices()) { 143 | double d = vert.subtract(v).lengthSquared(); 144 | if (d < dist) { 145 | bestVertex = vert; 146 | dist = d; 147 | } 148 | } 149 | } 150 | return bestVertex; 151 | } 152 | 153 | public Set getVerticesInRadius(Vertex v, double radius) { 154 | Set checked = Sets.newHashSet(v); 155 | Set inRadius = Sets.newHashSet(v); 156 | Set toCheck = Sets.newHashSet(v.getNeighborVertices()); 157 | 158 | while (toCheck.size() > 0) { 159 | Vertex check = Iterables.getFirst(toCheck, null); 160 | toCheck.remove(check); 161 | checked.add(check); 162 | 163 | if (v.subtract(check).length() < radius) { 164 | inRadius.add(check); 165 | toCheck.addAll(check.getNeighborVertices()); 166 | toCheck.removeAll(checked); 167 | } 168 | } 169 | 170 | return inRadius; 171 | } 172 | 173 | /** 174 | * Creates a Delaunay Triangulation of the {@link Vertex}s. 175 | * 176 | * This implementation uses a simple iterative approach, but with some 177 | * pre-processing to make the real-world performance fast. 178 | * 179 | * First, the vertices are sorted using a Hilbert Space-Filling curve. Since 180 | * our locate method walks the triangulation, linearizing the points with a 181 | * space-filling curve gives us some pretty good locality when adding each 182 | * vertex, thus greatly reducing the number of hops required to locate the 183 | * vertex. The sort is O(n log n), but is fast since hilbert indices are 184 | * computed in O(h) (where h is a small constant), and results in a 185 | * triangulation asymptotic running time of O(n) for non-diabolical cases. 186 | * For more info, see: {@link http://en.wikipedia.org/wiki/Hilbert_curve} 187 | * 188 | * For each vertex, we walk to the enclosing triangle. We create a cavity 189 | * from that triangle and all neighboring triangles for which the vertex is 190 | * in their circumcircle. 191 | * 192 | * Then, we create new triangles between the edges of the cavity and the 193 | * vertex. 194 | * 195 | * We guarantee that a triangle will be located for each vertex by first 196 | * creating a "super triangle" that is at least twice as large as the bounds 197 | * of the vertices. If this triangulation will be used for point location, 198 | * you will want to call {@link #setKeepSuperTriangle(true)} so that points 199 | * outside the convex hull of vertices may also be located. 200 | * 201 | * Basic incremental triangulation method inspired by Paul Bourke's notes 202 | * and psuedocode. See: {@link http://paulbourke.net/papers/triangulate/} 203 | * 204 | * @throws InvalidVertexException 205 | * if any two vertices have the same location or if any three 206 | * points are co-linear. 207 | */ 208 | public void triangulate() throws InvalidVertexException { 209 | /* 210 | * Reset triangulation state 211 | */ 212 | resetTriangulation(); 213 | 214 | if (Iterables.isEmpty(inputVertices)) { 215 | return; 216 | } 217 | 218 | /* 219 | * Determine the supertriangle. 220 | */ 221 | createSuperTriangle(inputVertices); 222 | 223 | /* 224 | * Sort vertices using hilbert curve to linearize triangulation 225 | * performance. 226 | */ 227 | log.debug("Linearizing with Hilbert Space-filling Curve"); 228 | List sortedVertices = getHilbertSortedVertices(inputVertices); 229 | 230 | /* 231 | * Add vertices one at a time, updating the triangulation as we go. 232 | */ 233 | log.debug("Building Triangulation"); 234 | for (Vertex vertex : sortedVertices) { 235 | try { 236 | addVertexToTriangulation(vertex); 237 | } catch (InvalidVertexException e) { 238 | if (vertexExceptionStrategy == VertexExceptionStrategy.THROW_EXCEPTION) { 239 | throw e; 240 | } else { 241 | // ignore 242 | } 243 | } 244 | } 245 | 246 | /* 247 | * Cleanup 248 | */ 249 | clearLocator(); 250 | if (!keepSuperTriangle) { 251 | removeSuperTriangle(); 252 | } 253 | 254 | log.debug("Triangulation Complete"); 255 | } 256 | 257 | public void clear() { 258 | resetTriangulation(); 259 | inputVertices = Sets.newLinkedHashSet(); 260 | } 261 | 262 | private void resetTriangulation() { 263 | triangles = Sets.newLinkedHashSet(); 264 | vertices = Sets.newLinkedHashSet(); 265 | clearLocator(); 266 | } 267 | 268 | private List getHilbertSortedVertices(Iterable verts) { 269 | Rectangle2D bbox = Vectors.boundingBox(Lists.newArrayList(superVerts)); 270 | ScaledHilbertIndex hilbert = new ScaledHilbertIndex(hilbertOrder, bbox); 271 | for (Vertex v : verts) { 272 | v.setHilbertIndex(hilbert.toIndex(v.x, v.y)); 273 | } 274 | List sortedVertices = Lists.newArrayList(verts); 275 | Collections.sort(sortedVertices, new Comparator() { 276 | public int compare(Vertex v1, Vertex v2) { 277 | return v1.getHilbertIndex().compareTo(v2.getHilbertIndex()); 278 | } 279 | }); 280 | return sortedVertices; 281 | } 282 | 283 | public void addVertexToTriangulation(Vertex vertex) throws InvalidVertexException { 284 | Collection toRemove = null, toAdd = null; 285 | 286 | try { 287 | /* 288 | * Get the set of triangles for which the vertex lies in its 289 | * circumcircle. 290 | */ 291 | toRemove = getCircumcircleTriangles(vertex); 292 | } catch (NonDelaunayException e) { 293 | /* 294 | * Unfortunately, we cannot recover from this state since the 295 | * triangulation is already non-delaunay. It was probably caused 296 | * by overlapping vertices, so we throw an invalid vertex 297 | * exception. 298 | */ 299 | throw new InvalidVertexException(); 300 | } catch (InvalidVertexException e) { 301 | log.debug(String.format("Dropping vertex %s because it outside the triangulation!\nMaybe something went wrong when computing the super triangle?", vertex)); 302 | return; 303 | } 304 | 305 | /* 306 | * Compute the set of edges that represent the convex hull of the 307 | * cavity left by removing the triangles. 308 | */ 309 | List edgeSet = getEdgeSet(toRemove); 310 | 311 | /* 312 | * Remove the triangles. 313 | */ 314 | removeTriangles(toRemove); 315 | 316 | try { 317 | /* 318 | * Create and add triangles created from the cavity convex hull 319 | * edges and the vertex. 320 | */ 321 | toAdd = createTriangles(edgeSet, vertex); 322 | addTriangles(toAdd); 323 | vertices.add(vertex); 324 | } catch (NonDelaunayException e) { 325 | log.debug(String.format("Dropping vertex %s because it causes degeneracy.\nYou may need to use exact math on this vertex.", vertex)); 326 | removeTriangles(toAdd); 327 | addTriangles(toRemove); 328 | } 329 | } 330 | 331 | public void createSuperTriangle(Iterable verts) { 332 | createSuperTriangle(Vectors.boundingBox(verts)); 333 | } 334 | 335 | public void createSuperTriangle(Rectangle2D rect) { 336 | double dmax = Math.max(rect.getWidth(), rect.getHeight()); 337 | double xmid = rect.getCenterX(); 338 | double ymid = rect.getCenterY(); 339 | 340 | superVerts = new Vertex[] { 341 | new Vertex(xmid - dmax * superTriangleScale, ymid - dmax), 342 | new Vertex(xmid, ymid + dmax * superTriangleScale), 343 | new Vertex(xmid + dmax * superTriangleScale, ymid - dmax) 344 | }; 345 | 346 | triangles = Sets.newLinkedHashSet(); 347 | triangles.add(new Triangle(superVerts[0], superVerts[1], superVerts[2])); 348 | } 349 | 350 | public void removeSuperTriangle() { 351 | Set touching = Sets.newHashSet(); 352 | for (Vertex v : superVerts) { 353 | touching.addAll(v.getNeighborTriangles()); 354 | } 355 | removeTriangles(touching); 356 | superVerts = new Vertex[] {}; 357 | } 358 | 359 | public boolean touchesSuperVertex(Triangle tri) { 360 | for (Vertex v : superVerts) { 361 | if (tri.getVertices().contains(v)) { 362 | return true; 363 | } 364 | } 365 | return false; 366 | } 367 | 368 | public boolean neighborsSuperVertex(Vertex vert) { 369 | for (Triangle tri : vert.getNeighborTriangles()) { 370 | if (touchesSuperVertex(tri)) { 371 | return true; 372 | } 373 | } 374 | if (Sets.newHashSet(superVerts).contains(vert)) { 375 | return true; 376 | } 377 | return false; 378 | } 379 | 380 | private void clearLocator() { 381 | lastLocatedTriangle = null; 382 | } 383 | 384 | public Collection getCircumcircleTriangles(Vector vertex) throws InvalidVertexException, NonDelaunayException { 385 | Triangle t = locate(vertex); 386 | if (t == null) { 387 | throw new InvalidVertexException(); 388 | } 389 | return getCircumcircleTriangles(vertex, t); 390 | } 391 | 392 | private Collection getCircumcircleTriangles(Vector vertex, Triangle t) { 393 | Set checked = Sets.newHashSet(t); 394 | Set inCircum = Sets.newHashSet(t); 395 | 396 | // Initialize "to check" set with neighbors 397 | Set toCheck = Sets.newHashSet(t.getNeighbors()); 398 | 399 | // For first triangle in "to check" set, check if 400 | // the vertex is in its circum circle. 401 | while (toCheck.size() > 0) { 402 | t = Iterables.getFirst(toCheck, null); 403 | toCheck.remove(t); 404 | 405 | if (t.isInCircum(vertex)) { 406 | inCircum.add(t); 407 | // If it is, add *its* neighbors to the "to check" set. 408 | toCheck.addAll(t.getNeighbors()); 409 | toCheck.removeAll(checked); 410 | } 411 | checked.add(t); 412 | } 413 | 414 | return inCircum; 415 | } 416 | 417 | /* 418 | * Walks the triangulation toward the vertex. Returns the triangle in which 419 | * the vertex resides. If the vertex is outside the current triangulation, 420 | * nil is returned. 421 | * 422 | * It is possible that if the triangulation breaks due to floating point 423 | * errors, it will cause errors during locate. In this case, we throw a 424 | * NonDelaunayException. 425 | * 426 | * If the vertices are near each other, such as when iterating over a 427 | * hilbert linearization or running a scanline of locations, this should be 428 | * pretty fast. 429 | */ 430 | public Triangle locate(Vector v) throws NonDelaunayException { 431 | locateCount += 1; 432 | Triangle t = lastLocatedTriangle == null ? Iterables.getFirst(triangles, null) : lastLocatedTriangle; 433 | if (t == null) { 434 | return null; 435 | } 436 | boolean done = false; 437 | 438 | Set seen = Sets. newHashSet(); 439 | while (!done) { 440 | hopCount += 1; 441 | lastLocatedTriangle = t; 442 | if (seen.contains(t)) { 443 | throw new NonDelaunayException(); 444 | } 445 | seen.add(t); 446 | Triangle tNext = t.nextWalk(v); 447 | if (tNext == null) { 448 | return null; 449 | } 450 | done = (tNext == t); 451 | t = tNext; 452 | } 453 | 454 | /* 455 | * During triangulation the located triangle is immediately removed. 456 | * But, it can be useful to store this if we are locating points in the 457 | * triangulation after it's constructed. 458 | */ 459 | lastLocatedTriangle = t; 460 | return t; 461 | } 462 | 463 | public List getEdgeSet(Collection tris) { 464 | List edges = Lists.newArrayList(); 465 | for (Triangle t : tris) { 466 | for (Edge e : t.getEdges()) { 467 | if (edges.contains(e)) { 468 | edges.remove(e); 469 | } else { 470 | edges.add(e); 471 | } 472 | } 473 | } 474 | return edges; 475 | } 476 | 477 | public List createTriangles(Iterable edgeSet, final Vertex vertex) { 478 | return Lists.newArrayList(Iterables.transform(edgeSet, new Function() { 479 | public Triangle apply(Edge e) { 480 | return new Triangle(vertex, e.a, e.b); 481 | } 482 | })); 483 | } 484 | 485 | private void addTriangles(Iterable tris) throws NonDelaunayException { 486 | for (Triangle t : tris) { 487 | for (Vertex v : t.getVertices()) { 488 | v.addTriangle(t); 489 | } 490 | triangles.add(t); 491 | } 492 | lastLocatedTriangle = Iterables.getFirst(tris, null); 493 | 494 | // uncomment to debug robustness issues at the cost of performance 495 | // triangles.each { |t| raise NonDelaunayException unless t.delaunay? } 496 | } 497 | 498 | private void removeTriangles(Iterable tris) { 499 | for (Triangle t : tris) { 500 | for (Vertex v : t.getVertices()) { 501 | v.removeTriangle(t); 502 | } 503 | triangles.remove(t); 504 | } 505 | lastLocatedTriangle = null; 506 | } 507 | } 508 | --------------------------------------------------------------------------------