
org.dyn4j.geometry.simplify.DouglasPeucker Maven / Gradle / Ivy
Show all versions of dyn4j Show documentation
/*
* Copyright (c) 2010-2022 William Bittle http://www.dyn4j.org/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
* and the following disclaimer in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or
* promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.dyn4j.geometry.simplify;
import java.util.ArrayList;
import java.util.List;
import org.dyn4j.exception.ValueOutOfRangeException;
import org.dyn4j.geometry.Vector2;
/**
* Simple polygon (without holes) simplifier that reduces the number of vertices by
* removing points that are less than epsilon distance away from a guide line.
*
* NOTE: This algorithm is designed for polylines, but has been adapted for simple
* polygons without holes by first sub-dividing the polygon into two polylines.
* This first sub-division process will always be from the first vertex in the polygon
* to the vertex farthest from the first vertex.
*
* The guide line is defined by the line from the start to the end of the polyline
* being processed. If all points between the start and end point of the polyline are
* epsilon or more distant from the guide line, the algorithm splits the polyline
* and processes each part. This continues recursively until the algorithm is complete.
*
* This algorithm has O(n log n) complexity where n is the number of vertices in the source
* polygon. This algorithm prevents self-intersections arising from the simplification
* process by skipping the simplification.
*
* This method does not require the polygon to have any defined winding, but does assume
* that it does not have holes and is not self-intersecting.
*
* This method handles null/empty lists, null elements, and all null elements. In these
* cases it's possible the returned list will be empty or have less than 3 vertices.
*
* NOTE: This algorithm's result is highly dependent on the given cluster tolerance, epsilon
* and the input polygon. There's no guarantee that the result will have 3 or more vertices.
* @author William Bittle
* @version 5.0.0
* @since 4.2.0
* @see Vertex Cluster Reduction
*/
public final class DouglasPeucker extends VertexClusterReduction implements Simplifier {
/** The minimum distance epsilon */
private final double epsilon;
/**
* Minimal constructor.
* @param clusterTolerance the cluster tolerance; must be zero or greater
* @param epsilon the minimum distance epsilon; must be zero or greater
* @throws IllegalArgumentException if clusterTolerance is less than zero or epsilon is less than zero
*/
public DouglasPeucker(double clusterTolerance, double epsilon) {
super(clusterTolerance);
if (epsilon < 0)
throw new ValueOutOfRangeException("epsilon", epsilon, ValueOutOfRangeException.MUST_BE_GREATER_THAN_OR_EQUAL_TO, 0);
this.epsilon = epsilon;
}
/* (non-Javadoc)
* @see org.dyn4j.geometry.simplify.VertexClusterReduction#simplify(java.util.List)
*/
public List simplify(List vertices) {
if (vertices == null) {
return vertices;
}
// first simplify via vertex clustering
vertices = super.simplify(vertices);
if (vertices.size() < 4) {
return vertices;
}
// build a linked list of vertices
List verts = this.buildVertexList(vertices);
SegmentTree tree = this.buildSegmentTree(verts.get(0));
// 1. find two points to split the poly into two halves
// for example:
// 0-------------0 A-------------0 | 0
// | \ | | \
// | \ => | & \
// | \ | | \
// 0-----------------0 0 | 0-----------------B
int startIndex = 0;
int endIndex = this.getFartherestVertexFromVertex(startIndex, vertices);
// 2. split into two polylines to simplify
List aReduced = this.douglasPeucker(verts.subList(startIndex, endIndex + 1), tree);
List bReduced = this.douglasPeucker(verts.subList(endIndex, vertices.size()), tree);
// 3. merge the two polylines back together
List result = new ArrayList();
result.addAll(aReduced.subList(0, aReduced.size() - 1));
result.addAll(bReduced);
return result;
}
/**
* Recursively sub-divide the given polyline performing the douglas Peucker algorithm.
*
* O(mn) in worst case, O(n log m) in best case, where n is the number of vertices in the
* original polyline and m is the number of vertices in the reduced polyline.
* @param polyline
* @return List<{@link Vector2}>
*/
private final List douglasPeucker(List polyline, SegmentTree tree) {
int size = polyline.size();
List result = new ArrayList();
// can't do anything with 1 or 2 points - we just have to keep them
if (size < 3) {
for (int i = 0; i < size; i++) {
result.add(polyline.get(i).point);
}
return result;
}
// get the start/end vertices of the polyline
SimplePolygonVertex sv = polyline.get(0);
SimplePolygonVertex ev = polyline.get(size - 1);
// get the farthest vertex from the line created from the start to the end
// vertex on the polyline
FarthestVertex fv = this.getFarthestVertexFromLine(sv, ev, polyline);
// check the farthest point's distance - if it's higher than the minimum
// distance epsilon, then we need to subdivide the polyline since we can't
// reduce here (we might be able to reduce elsewhere)
if (fv.distance >= epsilon) {
// sub-divide and run the algo on each half
List aReduced = this.douglasPeucker(polyline.subList(0, fv.index + 1), tree);
List bReduced = this.douglasPeucker(polyline.subList(fv.index, size), tree);
// recombine the reduced polylines
result.addAll(aReduced.subList(0, aReduced.size() - 1));
result.addAll(bReduced);
} else {
// check for self-intersection
if (this.isSelfIntersectionProduced(sv, ev, tree)) {
// if removing all the points between v1 and v2 produces self-intersection
// then we can either stop and all points between v1 and v2 to the result
// or we can split the polyline by the farthest point and try to simplify
// those sub-polylines
// sub-divide and run the algo on each half
List aReduced = this.douglasPeucker(polyline.subList(0, fv.index + 1), tree);
List bReduced = this.douglasPeucker(polyline.subList(fv.index, size), tree);
// recombine the reduced polylines
result.addAll(aReduced.subList(0, aReduced.size() - 1));
result.addAll(bReduced);
return result;
}
// if there's no self-intersection, then we need to remove
// all segments from the segment tree in between these vertices
SimplePolygonVertex b = sv;
while (b != ev) {
tree.remove(b.nextSegment);
b = b.next;
}
// remove all the vertices between sv/ev
sv.next = ev;
ev.prev = sv;
// create a new segment between sv/ev
sv.nextSegment = new SegmentTreeLeaf(sv.point, ev.point, sv.index, ev.index);
ev.prevSegment = sv.nextSegment;
// add the new segment to the segment tree
tree.add(sv.nextSegment);
// just use the start/end vertices
// as the result
result.add(sv.point);
result.add(ev.point);
}
return result;
}
/**
* Returns the vertex farthest from the given vertex.
*
* O(n)
* @param index the vertex index
* @param polygon the entire polygon
* @return int
*/
private final int getFartherestVertexFromVertex(int index, List polygon) {
double dist2 = 0.0;
int max = -1;
int size = polygon.size();
Vector2 vertex = polygon.get(index);
for (int i = 0; i < size; i++) {
Vector2 vert = polygon.get(i);
double test = vertex.distanceSquared(vert);
if (test > dist2) {
dist2 = test;
max = i;
}
}
return max;
}
/**
* Returns the farthest vertex in the polyline from the line created by lineVertex1 and lineVertex2.
*
* O(n)
* @param lineVertex1 the first vertex of the line
* @param lineVertex2 the second vertex of the line
* @param polyline the entire polyline
* @return {@link FarthestVertex}
*/
private final FarthestVertex getFarthestVertexFromLine(SimplePolygonVertex lineVertex1, SimplePolygonVertex lineVertex2, List polyline) {
int index = -1;
double distance = 0.0;
Vector2 lp1 = lineVertex1.point;
Vector2 lp2 = lineVertex2.point;
// find the vertex on the polyline that's farthest from the line created
// by lineVertex1 and lineVertex2
int size = polyline.size();
Vector2 line = lp1.to(lp2);
Vector2 lineNormal = line.getLeftHandOrthogonalVector();
lineNormal.normalize();
for (int i = 0; i < size; i++) {
Vector2 vert = polyline.get(i).point;
double test = Math.abs(lp1.to(vert).dot(lineNormal));
if (test > distance) {
distance = test;
index = i;
}
}
// make sure we found a winner
if (index < 0) {
// then they were all colinear, so take the middle one
// NOTE: integer division here
index = size / 2;
distance = 0.0;
}
return new FarthestVertex(index, distance);
}
/**
* Represents the farthest vertex from a line.
* @author William Bittle
* @version 4.2.0
* @since 4.2.0
*/
private final class FarthestVertex {
/** The index */
final int index;
/** The distance */
final double distance;
/**
* Minimal constructor.
* @param index the index in the polyline
* @param distance the distance from the line
*/
public FarthestVertex(int index, double distance) {
this.index = index;
this.distance = distance;
}
}
}