Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
package com.marginallyclever.convenience.linecollection;
import com.marginallyclever.convenience.LineSegment2D;
import org.jetbrains.annotations.NotNull;
import javax.vecmath.Point2d;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* <p>{@link RamerDouglasPeuckerRecursive} simplifies a {@link LineCollection} using the <a
* href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm">Ramer-Douglas-Peucker
* algorithm</a> in a recursive fashion.</p>
* <p>Requires all points are contiguous and of the same color.</p>
*/
public class RamerDouglasPeuckerRecursive implements LineSimplifier {
private final double CONTIGUOUS_TOLERANCE = 1e-4;
private final List<Point2d> points = new ArrayList<>();
private final boolean[] keep;
private final Color c;
private double distanceToleranceSq;
/**
* Construct a new {@link RamerDouglasPeuckerRecursive} object.
* @param lineCollection the line collection to simplify
* @throws IllegalArgumentException if the segments are not contiguous
*/
public RamerDouglasPeuckerRecursive(@NotNull LineCollection lineCollection) throws IllegalArgumentException {
testSegmentsAreContiguousAndSameColor(lineCollection);
c = lineCollection.getFirst().color;
points.add(lineCollection.getFirst().start);
for (var line : lineCollection) {
points.add(line.end);
}
keep = new boolean[points.size()];
}
@Override
public @NotNull LineCollection simplify(double distanceTolerance) {
var len = points.size();
keep[0] = true;
keep[len-1] = true;
distanceToleranceSq = distanceTolerance * distanceTolerance;
try {
if(lineIsClosedLoop()) {
// if this is a closed loop the ptLineDistSq test will fail, so we need the first point
// and the point as far from first point as possible, then test in two halves.
int furthestIndex = getFurthestPointFromStart();
// if the furthest point is the first or last point, we can't simplify.
if(furthestIndex!=0 && furthestIndex!=len-1) {
keep[furthestIndex] = true;
simplifySection(0,furthestIndex);
simplifySection(furthestIndex,len-1);
}
} else {
simplifySection(0, len - 1);
}
} catch (Exception e) {
e.printStackTrace();
}
return assembleResult();
}
private int getFurthestPointFromStart() {
var a = points.getFirst();
double maxDistanceSq = 0;
int maxIndex = 0;
for (int k = 1; k < points.size()-1; k++) {
double distSq = a.distanceSquared(points.get(k));
if (distSq > maxDistanceSq) {
maxDistanceSq = distSq;
maxIndex = k;
}
}
return maxIndex;
}
private boolean lineIsClosedLoop() {
return points.getFirst().distanceSquared(points.getLast()) < CONTIGUOUS_TOLERANCE;
}
private @NotNull LineCollection assembleResult() {
LineCollection result = new LineCollection();
var head = points.getFirst();
var len = points.size();
for (int i = 1; i < len; i++) {
if (keep[i]) {
var next = points.get(i);
result.add(new LineSegment2D(head, next, c));
head = next;
}
}
return result;
}
/**
* Test that the segments are contiguous.
* @throws IllegalArgumentException if the segments are not contiguous
*/
private void testSegmentsAreContiguousAndSameColor(LineCollection list) throws IllegalArgumentException {
var len = list.size();
if(len<2) return;
var first = list.getFirst();
Color firstColor = first.color;
// test that the end of each original line is the start of the next
for (int i = 0; i < len-1; i++) {
var next = list.get(i+1);
if( first.end.distanceSquared(next.start) > CONTIGUOUS_TOLERANCE ) {
throw new IllegalArgumentException("Line segments must be contiguous.");
}
if( !firstColor.equals(next.color) ) {
throw new IllegalArgumentException("Line segments must be the same color.");
}
first = next;
}
}
/**
* <p>Simplify the line collection by removing points that are within distanceTolerance of the line between their neighbors.</p>
* <p>The strategy is to split the work at the point that is farthest from the line between the start and end points,
* then try to simplify the two halves.</p>
* @param start the start index
* @param end the end index
*/
private void simplifySection(int start, int end) {
if(start + 1 >= end) return; // adjacent points, nothing to simplify
var a = points.get(start);
var b = points.get(end);
// find the point that is farthest from the line between the start and end points.
double maxDistanceSq = 0;
int maxIndex = -1;
for (int k = start+1; k < end; k++) {
var p = points.get(k);
double distSq = java.awt.geom.Line2D.ptLineDistSq(a.x,a.y, b.x,b.y, p.x,p.y);
if (distSq > maxDistanceSq) {
maxDistanceSq = distSq;
maxIndex = k;
}
}
if (maxDistanceSq > distanceToleranceSq && maxIndex>-1) {
// Split the work at the point of greatest inflection.
// Keep the point and simplify the two halves.
keep[maxIndex] = true;
simplifySection(start, maxIndex);
simplifySection(maxIndex, end);
} // else all points between start and end are within tolerance, they are already marked as false (discard).
}
}
74469e1ccThis file is pinned to the selected release. Download the original to open it in your design tool.