/*
* The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
* for visualizing and manipulating spatial features with geometry and attributes.
*
* Copyright (C) 2003 Vivid Solutions
*
* This program is 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 2
* of the License, or (at your option) any later version.
*
* This program is 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 program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* For more information, contact:
*
* Vivid Solutions
* Suite #1A
* 2328 Government Street
* Victoria BC V8T 5G5
* Canada
*
* (250)385-6040
* www.vividsolutions.com
*/
package com.vividsolutions.jump.warp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.util.Assert;
import com.vividsolutions.jump.task.TaskMonitor;
import com.vividsolutions.jump.util.CollectionMap;
/**
* A better name for this class would have been TriangleMapFactory. Given the
* coordinates of an initial and final triangulation, it will return a map of source
* Triangle to destination Triangle.
*
* Creates a FeatureCollection of triangles covering a given area. Thin
* triangles are avoided. <p>
*
* Coordinates are not created, modified, or discarded. Thus, the triangles
* created will be composed of the Coordinates passed in to the Triangulator.
*
* See White, Marvin S., Jr. and Griffin, Patricia. 1985. Piecewise linear
* rubber-sheet map transformation. "The American Cartographer" 12:2,
* 123-31.
*/
public class Triangulator {
private GeometryFactory factory = new GeometryFactory();
private Collection ignoredVectors = new ArrayList();
public Triangulator() {}
/**
* Splits two regions into Triangles. The two regions are called the
* "source quadrilateral" and "destination quadrilateral", and are based on
* the given dataset envelope. The "source quadrilateral" is the dataset envelope
* expanded 5% along each margin. The "destination quadrilateral" is the
* source quadrilateral with each vertex shifted according to the vector with
* the nearest tail. The source quadrilateral is split using the vector tails;
* the destination quadrilateral is split using the vector tips. In this way,
* the vectors map the source Triangles to the destination Triangles.
* @param datasetEnvelope the region to triangulate
* @param vectorLineStrings vectors (2-point LineStrings) whose tails and tips split
* the "source quadrilateral" and "destination quadrilateral" into triangles
* @param monitor
* @return a map of source Triangles to destination Triangles
*/
public Map triangleMap(
Envelope datasetEnvelope,
Collection vectorLineStrings,
TaskMonitor monitor) {
return triangleMap(
datasetEnvelope,
vectorLineStrings,
new ArrayList(),
new ArrayList(),
monitor);
}
/**
* @param sourceHints "far-away" Coordinates (even outside the dataset envelope) for which
* we must ensure that source triangles include.
* @param destinationHints "far-away" Coordinates for which we must ensure that destination
* triangles include
*/
public Map triangleMap(
Envelope datasetEnvelope,
Collection vectorLineStrings,
Collection sourceHints,
Collection destinationHints,
TaskMonitor monitor) {
ArrayList vectorListCopy = new ArrayList(vectorLineStrings);
ignoredVectors = nonVectors(vectorListCopy);
vectorListCopy.removeAll(ignoredVectors);
//Refinement on White & Griffin's algorithm: Bring outlying vectors back inside
//by gradually increasing the size of the source quad. This is a courtesy to
//the caller because really there shouldn't be any outlying vectors. [Jon Aquino]
Assert.isTrue(!datasetEnvelope.isNull());
Envelope sourceEnvelope = new Envelope(datasetEnvelope);
Quadrilateral sourceQuad;
Quadrilateral destQuad;
// int count=0;
while (true) {
//#sourceQuad will grow the envelope by 5%. [Jon Aquino]
sourceQuad = sourceQuad(sourceEnvelope);
destQuad = destQuad(sourceQuad, vectorListCopy);
//sstein[30.March.2008] -- note.. this loop will run endless
// if we try to warp a single point. therefore we check if
// the envelope truly grows. It can't grow for dx=dy=0
if ((sourceEnvelope.getWidth() == 0.0) && (sourceEnvelope.getHeight() == 0.0)){
break;
}
//--
if (outlyingVectors(sourceQuad, destQuad, vectorListCopy).isEmpty()
&& sourceQuad.verticesOutside(sourceHints).isEmpty()
&& destQuad.verticesOutside(destinationHints).isEmpty()) {
break;
}
// else{
// System.out.print("."); count=count+1;
// if ((count/50.0) == (Math.floor(count/50.0))){
// System.out.println(" " + count);
// }
// }
sourceEnvelope = sourceQuad.getEnvelope();
}
Quadrilateral taggedSourceQuad = tag(sourceQuad, destQuad);
List taggedSourceTriangles =
triangulate(taggedSourceQuad, taggedVectorVertices(false, vectorListCopy), monitor);
return triangleMap(taggedSourceTriangles);
}
/**
* Permits the caller to identify which vectors were ignored because they
* were not 2-point LineStrings
*/
public Collection getIgnoredVectors() {
return Collections.unmodifiableCollection(ignoredVectors);
}
public static Collection nonVectors(Collection geometries) {
TreeSet nonVectors = new TreeSet();
for (Iterator i = geometries.iterator(); i.hasNext();) {
Geometry g = (Geometry) i.next();
if (vector(g)) {
continue;
}
nonVectors.add(g);
}
return nonVectors;
}
public static boolean vector(Geometry g) {
return (g.getClass() == LineString.class) && (((LineString) g).getNumPoints() == 2);
}
/**
* @return vectors with the tail outside sourceQuad or the
* tip outside destQuad
*/
private TreeSet outlyingVectors(
Quadrilateral sourceQuad,
Quadrilateral destQuad,
Collection vectors) {
TreeSet outliers = new TreeSet();
outliers.addAll(
toVectors(sourceQuad.verticesOutside(taggedVectorVertices(false, vectors)), false));
outliers.addAll(
toVectors(destQuad.verticesOutside(taggedVectorVertices(true, vectors)), true));
return outliers;
}
/**
* The intent of this method is to avoid narrow triangles, which create near
* singularities.
*
*@param PQS a triangle sharing an edge with QRS; vertex order is irrelevant
*@return (PQS and QRS) or (PQR, PRS), whichever pair has the largest
* minimum height
*/
protected List heightMaximizedTriangles(Triangle PQS, Triangle QRS) {
List originalTriangles = Arrays.asList(new Triangle[] { PQS, QRS });
List alternativeTriangles = alternativeTriangles(PQS, QRS);
if (alternativeTriangles == null) {
return originalTriangles;
}
Triangle t1 = (Triangle) alternativeTriangles.get(0);
Triangle t2 = (Triangle) alternativeTriangles.get(1);
if (Math.min(PQS.getMinHeight(), QRS.getMinHeight())
> Math.min(t1.getMinHeight(), t2.getMinHeight())) {
return originalTriangles;
} else {
return alternativeTriangles;
}
}
/**
*@return the triangle containing p, or null if no triangle contains p
*/
protected Triangle triangleContaining(Coordinate p, List triangles) {
for (Iterator i = triangles.iterator(); i.hasNext();) {
Triangle triangle = (Triangle) i.next();
if (triangle.contains(p)) {
return triangle;
}
}
return null;
}
/**
*@return a + the displacement represented by vector
*/
protected Coordinate add(Coordinate a, LineString vector) {
return new Coordinate(
(a.x + vector.getCoordinateN(1).x) - vector.getCoordinateN(0).x,
(a.y + vector.getCoordinateN(1).y) - vector.getCoordinateN(0).y);
}
protected LineString vectorWithNearestTail(Coordinate x, List vectors) {
Assert.isTrue(vectors.size() > 0);
LineString vectorWithNearestTail = (LineString) vectors.get(0);
for (Iterator i = vectors.iterator(); i.hasNext();) {
LineString candidate = (LineString) i.next();
if (candidate.getCoordinateN(0).distance(x)
< vectorWithNearestTail.getCoordinateN(0).distance(x)) {
vectorWithNearestTail = candidate;
}
}
return vectorWithNearestTail;
}
/**
*@return sourceQuad wrapped in TaggedCoordinates pointing to the
* corresponding Coordinates in destQuad.
*/
protected Quadrilateral tag(Quadrilateral sourceQuad, Quadrilateral destQuad) {
return new Quadrilateral(
new TaggedCoordinate(sourceQuad.getP1(), destQuad.getP1()),
new TaggedCoordinate(sourceQuad.getP2(), destQuad.getP2()),
new TaggedCoordinate(sourceQuad.getP3(), destQuad.getP3()),
new TaggedCoordinate(sourceQuad.getP4(), destQuad.getP4()));
}
/**
*@param PQS a triangle sharing an edge with QRS; vertex order is irrelevant
*@return triangles PQR and PRS, or null if PQRS is not convex
*/
protected List alternativeTriangles(Triangle PQS, Triangle QRS) {
Quadrilateral quad = dissolve(PQS, QRS);
if (!quad.isConvex()) {
return null;
}
return quad.triangles();
}
/**
*@return a rectangle 5% larger along each margin
*@see White and Griffin's paper
*/
private Quadrilateral sourceQuad(Envelope datasetEnvelope) {
double dx = datasetEnvelope.getWidth() * 0.05;
double dy = datasetEnvelope.getHeight() * 0.05;
return new Quadrilateral(
new Coordinate(datasetEnvelope.getMinX() - dx, datasetEnvelope.getMinY() - dy),
new Coordinate(datasetEnvelope.getMaxX() + dx, datasetEnvelope.getMinY() - dy),
new Coordinate(datasetEnvelope.getMaxX() + dx, datasetEnvelope.getMaxY() + dy),
new Coordinate(datasetEnvelope.getMinX() - dx, datasetEnvelope.getMaxY() + dy));
}
/**
* Modifies the triangle list to accomodate the new vertex.
*/
private void triangulate(List triangles, Coordinate newVertex) {
Triangle triangleContainingNewVertex = triangleContaining(newVertex, triangles);
Assert.isTrue(triangleContainingNewVertex != null);
triangles.remove(triangleContainingNewVertex);
//Don't add triangles immediately, as we want #adjacentTriangle to return
//a triangle that isn't one of the split triangles. [Jon Aquino]
ArrayList trianglesToAdd = new ArrayList();
for (Iterator i = triangleContainingNewVertex.subTriangles(newVertex).iterator();
i.hasNext();
) {
Triangle newTriangle = (Triangle) i.next();
Triangle adjacentTriangle = adjacentTriangle(newTriangle, triangles);
if (adjacentTriangle == null) {
//that is, a boundary triangle [Jon Aquino]
trianglesToAdd.add(newTriangle);
} else {
triangles.remove(adjacentTriangle);
trianglesToAdd.addAll(heightMaximizedTriangles(newTriangle, adjacentTriangle));
}
}
triangles.addAll(trianglesToAdd);
}
/**
*@return the triangle adjacent to the given triangle, or null if there is
* none
*/
private Triangle adjacentTriangle(Triangle triangle, List triangles) {
for (Iterator i = triangles.iterator(); i.hasNext();) {
Triangle candidate = (Triangle) i.next();
int vertexMatches = 0;
if (candidate.hasVertex(triangle.getP1())) {
vertexMatches++;
}
if (candidate.hasVertex(triangle.getP2())) {
vertexMatches++;
}
if (candidate.hasVertex(triangle.getP3())) {
vertexMatches++;
}
Assert.isTrue(vertexMatches != 3, candidate + "; " + triangle);
if (vertexMatches == 2) {
return candidate;
}
}
return null;
}
/**
*@return sourceQuad, with each vertex shifted according to the vector with
* the nearest tail
*@see White and Griffin's paper
*/
private Quadrilateral destQuad(Quadrilateral sourceQuad, List vectors) {
if (vectors.isEmpty()) { return (Quadrilateral) sourceQuad.clone(); }
return new Quadrilateral(
addVectorWithNearestTail(sourceQuad.getP1(), vectors),
addVectorWithNearestTail(sourceQuad.getP2(), vectors),
addVectorWithNearestTail(sourceQuad.getP3(), vectors),
addVectorWithNearestTail(sourceQuad.getP4(), vectors));
}
private Coordinate addVectorWithNearestTail(Coordinate x, List vectors) {
return add(x, vectorWithNearestTail(x, vectors));
}
/**
*@param quad quadrilateral region to triangulate
*@param vertices triangle vertices; Coordinate objects, all within the
* quadrilateral region (use #containsAll to check)
*@return the triangles; Triangle objects
*@throws JUMPException if one or more vertices are outside the quadrilateral
* region
*/
private List triangulate(Quadrilateral quad, List vertices, TaskMonitor monitor) {
monitor.allowCancellationRequests();
monitor.report("Triangulating...");
List triangles = quad.triangles();
int count = 0;
for (Iterator i = vertices.iterator(); i.hasNext() && !monitor.isCancelRequested();) {
Coordinate vertex = (Coordinate) i.next();
triangulate(triangles, vertex);
count++;
monitor.report(count, vertices.size(), "vectors");
}
return triangles;
}
/**
* The returned Coordinates will be tagged with the tails if the tips are
* requested (or the tips, if the tails are requested).
*
*@param tips true to return the vector tips; otherwise, the tails
*/
public static List taggedVectorVertices(boolean tips, Collection vectors) {
ArrayList taggedVectorVertices = new ArrayList();
for (Iterator i = vectors.iterator(); i.hasNext();) {
LineString vector = (LineString) i.next();
taggedVectorVertices.add(
new TaggedCoordinate(
tips ? vector.getCoordinateN(1) : vector.getCoordinateN(0),
tips ? vector.getCoordinateN(0) : vector.getCoordinateN(1)));
}
return taggedVectorVertices;
}
private Map triangleMap(List taggedSourceTriangles) {
HashMap triangleMap = new HashMap();
for (Iterator i = taggedSourceTriangles.iterator(); i.hasNext();) {
Triangle sourceTriangle = (Triangle) i.next();
triangleMap.put(
sourceTriangle,
new Triangle(
((TaggedCoordinate) sourceTriangle.getP1()).getTag(),
((TaggedCoordinate) sourceTriangle.getP2()).getTag(),
((TaggedCoordinate) sourceTriangle.getP3()).getTag()));
}
return triangleMap;
}
/**
* @param tips true if c is the tip and c's tag is the tail; false if
* c is the tail and c's tag is the tip
*/
private LineString toVector(TaggedCoordinate c, boolean tips) {
//Constructor requires the tail followed by the tip.
return factory.createLineString(
new Coordinate[] { tips ? c.getTag() : c, tips ? c : c.getTag()});
}
/**
* The first coordinate of the returned quadrilateral will be an "unshared"
* vertex; that is, one that is present in only one of the triangles.
*
*@param PQS a triangle that shares an edge with QRS. The order of the
* Coordinates does not matter.
*@return a quadrilateral (four Coordinates) formed from the two
* triangles
*/
private Quadrilateral dissolve(Triangle PQS, Triangle QRS) {
CollectionMap vertexListMap = new CollectionMap(TreeMap.class);
vertexListMap.addItem(PQS.getP1(), PQS.getP1());
vertexListMap.addItem(PQS.getP2(), PQS.getP2());
vertexListMap.addItem(PQS.getP3(), PQS.getP3());
vertexListMap.addItem(QRS.getP1(), QRS.getP1());
vertexListMap.addItem(QRS.getP2(), QRS.getP2());
vertexListMap.addItem(QRS.getP3(), QRS.getP3());
ArrayList sharedVertices = new ArrayList();
ArrayList unsharedVertices = new ArrayList();
for (Iterator i = vertexListMap.keySet().iterator(); i.hasNext();) {
Coordinate vertex = (Coordinate) i.next();
if (vertexListMap.getItems(vertex).size() == 1) {
unsharedVertices.add(vertex);
} else if (vertexListMap.getItems(vertex).size() == 2) {
sharedVertices.add(vertex);
} else {
Assert.shouldNeverReachHere();
}
}
Assert.isTrue(2 == sharedVertices.size(), PQS + "; " + QRS);
Assert.isTrue(2 == unsharedVertices.size(), PQS + "; " + QRS);
return new Quadrilateral(
(Coordinate) unsharedVertices.get(0),
(Coordinate) sharedVertices.get(0),
(Coordinate) unsharedVertices.get(1),
(Coordinate) sharedVertices.get(1));
}
private TreeSet toVectors(Collection taggedVectorVertices, boolean tips) {
TreeSet badVectors = new TreeSet();
for (Iterator i = taggedVectorVertices.iterator(); i.hasNext();) {
TaggedCoordinate c = (TaggedCoordinate) i.next();
badVectors.add(toVector(c, tips));
}
return badVectors;
}
}