/*
* This file is part of the Trickl Open Source Libraries.
*
* Trickl Open Source Libraries - http://open.trickl.com/
*
* Copyright (C) 2011 Tim Gee.
*
* Trickl Open Source Libraries are free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Trickl Open Source Libraries are distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this project. If not, see <http://www.gnu.org/licenses/>.
*/
package com.trickl.graph.planar;
import com.vividsolutions.jts.algorithm.Angle;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.algorithm.ConvexHull;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.geom.impl.CoordinateArraySequenceFactory;
import java.util.*;
import org.jgrapht.VertexFactory;
public class DelaunayVoronoiVisitor<V1, E1, V2, E2> extends DualGraphVisitor<V1, E1, V2, E2> {
final private PlanarLayout<V1> inputLayout;
final private PlanarLayoutStore<V2> outputLayout;
final private LinearRing boundary;
final private List<V2> boundaryVertices;
final private GeometryFactory geometryFactory;
/**
*
* @param inputGraph
* @param inputLayout
* @param dualGraph
* @param outputLayout
* @param boundary Outward facing boundary
* @param vertexFactory
*/
public DelaunayVoronoiVisitor(PlanarGraph<V1, E1> inputGraph,
PlanarLayout<V1> inputLayout,
PlanarGraph<V2, E2> dualGraph,
PlanarLayoutStore<V2> outputLayout,
LinearRing boundary,
VertexFactory<V2> vertexFactory) {
super(inputGraph, dualGraph, vertexFactory);
this.geometryFactory = new GeometryFactory();
if (inputLayout == null) {
throw new NullPointerException();
}
// Validate the boundary is defined clockwise
if (boundary != null) {
if (CGAlgorithms.signedArea(boundary.getCoordinates()) < 0)
{
throw new IllegalArgumentException("Boundary must be defined clockwise");
}
}
// Validate that dual graph boundary is convex
if (!PlanarGraphs.isBoundaryConvex(inputGraph, inputLayout)) {
throw new IllegalArgumentException("Delaunay graphs must have a convex boundary.");
}
this.inputLayout = inputLayout;
this.outputLayout = outputLayout;
this.boundary = boundary;
this.boundaryVertices = new ArrayList<V2>(boundary == null ? 0
: boundary.getCoordinates().length);
if (boundary != null) {
// Note that in a linear ring, end element equals start element
for (int i = 0; i < boundary.getCoordinates().length - 1; ++i) {
Coordinate coord = boundary.getCoordinateN(i);
V2 boundaryVertex = vertexFactory.createVertex();
boundaryVertices.add(boundaryVertex);
this.outputLayout.setCoordinate(boundaryVertex, coord);
this.dualGraph.addVertex(boundaryVertex);
}
}
}
@Override
public void beginFace(V1 source, V1 target) {
Coordinate dualTargetLocation = getDualLocation(source, target);
if (dualTargetLocation != null &&
CGAlgorithms.isPointInRing(dualTargetLocation, boundary.getCoordinates())) {
super.beginFace(source, target);
outputLayout.setCoordinate(dualTarget, dualTargetLocation);
}
}
@Override
public void nextEdge(V1 source, V1 target) {
if (dualTarget == null) {
Coordinate dualTargetLocation = getDualLocation(source, target);
Coordinate dualSourceLocation = getDualLocation(target, source);
if (dualSourceLocation != null) {
if (dualTargetLocation == null &&
// There's no dual target as this is the boundary of the graph
CGAlgorithms.isPointInRing(dualSourceLocation, boundary.getCoordinates())) {
createEdgeToBoundary(source, target);
}
else if (dualTargetLocation != null) {
// The dual target exists, but is outside the boundary
LineSegment line = new LineSegment(dualSourceLocation, dualTargetLocation);
if (boundary.intersects(line.toGeometry(geometryFactory))) {
createEdgeToBoundary(source, target);
}
}
}
} else {
super.nextEdge(source, target);
}
}
/**
* Get the location of the Voronoi vertex given a delaunay edge
*
* @param delaunaySource
* @param delaunayTarget
* @return If the corresponding vertex is inside the boundary, returns the
* coordinates of the vertex, otherwise returns null.
*/
private Coordinate getDualLocation(V1 delaunaySource, V1 delaunayTarget) {
if (inputGraph.isBoundary(delaunaySource, delaunayTarget)) {
return null;
}
Coordinate a = inputLayout.getCoordinate(delaunaySource);
Coordinate b = inputLayout.getCoordinate(delaunayTarget);
Coordinate c = inputLayout.getCoordinate(inputGraph.getNextVertex(delaunaySource, delaunayTarget));
// Inner faces should be defined counterclockwise by convention
Coordinate circumcentre = Triangle.circumcentre(a, b, c);
return circumcentre;
}
@Override
public void endTraversal() {
super.endTraversal();
// Assumes the boundary is convex so half lines should never intersect.
// Join up the boundary vertices, note the boundary faces
// outwards so we take care to create an inward face
Collections.reverse(boundaryVertices);
for (int prevItr = 0; prevItr < boundaryVertices.size(); ++prevItr) {
int itr = (prevItr + 1) % boundaryVertices.size();
int nextItr = (prevItr + 2) % boundaryVertices.size();
V2 boundaryPrevious = boundaryVertices.get(prevItr);
V2 boundarySource = boundaryVertices.get(itr);
V2 boundaryTarget = boundaryVertices.get(nextItr);
V2 boundaryBefore = null;
if (dualGraph.containsEdge(boundarySource, boundaryPrevious)) {
boundaryBefore = dualGraph.getPrevVertex(boundarySource, boundaryPrevious);
} else if (PlanarGraphs.isVertexBoundary(dualGraph, boundarySource)) {
boundaryBefore = PlanarGraphs.getPrevVertexOnBoundary(dualGraph, boundarySource);
}
E2 edge = edgeFactory.createEdge(boundarySource, boundaryTarget);
dualGraph.addEdge(boundarySource, boundaryTarget, boundaryBefore, null, edge);
}
}
private void createEdgeToBoundary(V1 source, V1 target) {
LineSegment halfLine = getHalfLineToBoundary(source, target);
int segmentIndex = PlanarGraphs.getNearestInterceptingLineSegment(halfLine, boundaryVertices, outputLayout);
LineSegment boundarySegment = PlanarGraphs.getLineSegment(segmentIndex, boundaryVertices, outputLayout);
Coordinate dualTargetLocation = boundarySegment.lineIntersection(halfLine);
super.beginFace(source, target);
boundaryVertices.add((segmentIndex + 1) % boundaryVertices.size(), dualTarget);
outputLayout.setCoordinate(dualTarget, dualTargetLocation);
super.nextEdge(source, target);
super.endFace(source, target);
}
private LineSegment getHalfLineToBoundary(V1 delaunaySource, V1 delaunayTarget) {
Coordinate dualSourceLocation = getDualLocation(delaunayTarget, delaunaySource);
Coordinate a = inputLayout.getCoordinate(delaunaySource);
Coordinate b = inputLayout.getCoordinate(delaunayTarget);
LineSegment ab = new LineSegment(a, b);
Coordinate midPoint = ab.midPoint();
Coordinate reflectedDualSourceLocation =
new Coordinate(2 * midPoint.x - dualSourceLocation.x,
2 * midPoint.y - dualSourceLocation.y);
return new LineSegment(midPoint, reflectedDualSourceLocation);
}
public static <V> List<V> getConvexBoundaryVertices(List<V> vertices, PlanarLayout<V> planarLayout) {
Map<Coordinate, V> coordinateToVertex = new HashMap<>();
CoordinateList flattenedLocations = new CoordinateList();
for (V vertex : vertices) {
Coordinate location = planarLayout.getCoordinate(vertex);
if (location != null) {
flattenedLocations.add(location);
coordinateToVertex.put(location, vertex);
}
}
CoordinateSequenceFactory coordinateSequenceFactory = CoordinateArraySequenceFactory.instance();
GeometryFactory geometryFactory = new GeometryFactory(coordinateSequenceFactory);
ConvexHull convexHull = new ConvexHull(flattenedLocations.toCoordinateArray(), geometryFactory);
Coordinate[] boundary = convexHull.getConvexHull().getCoordinates();
List<V> boundaryVertices = new LinkedList<>();
for (int i = 0; i < (boundary.length - 1); ++i) {
boundaryVertices.add(coordinateToVertex.get(boundary[i]));
}
return boundaryVertices;
}
}