org.geolatte.geom.cga.CircularArcLinearizer Maven / Gradle / Ivy
Show all versions of geolatte-geom Show documentation
package org.geolatte.geom.cga;
import org.geolatte.geom.Position;
import org.geolatte.geom.PositionSequence;
import org.geolatte.geom.PositionSequenceBuilder;
import java.util.Arrays;
import static java.lang.Math.*;
import static org.geolatte.geom.PositionSequenceBuilders.variableSized;
/**
* Linearizes arc segments defined by three consecutive {@code Positions}
*
* The implementation guarantees that the specified Positions are among the returned, linearized Positions
*
* Created by Karel Maesen, Geovise BVBA on 02/03/15.
*/
public class CircularArcLinearizer {
final private double threshold;
final private P p0;
final private P p1;
final private P p2;
final private Circle c;
final private NumericalMethods.Orientation orientation;
final private PositionSequenceBuilder
builder;
//TODO threshold must become parameter of linearize/linearizeCircle!!
// to that tolerance can be calculated as proportional to radius.
public CircularArcLinearizer(P p0, P p1, P p2, double threshold) {
if (p0 == null || p1 == null | p2 == null) {
throw new IllegalArgumentException();
}
this.threshold = abs(threshold);
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.c = new Circle(p0, p1, p2, false);
this.orientation = NumericalMethods.orientation(p0, p1, p2);
this.builder = variableSized((Class
) p0.getClass());
}
public Circle getCircle() {
return this.c;
}
public double getRadius() {
return this.c.radius;
}
public PositionSequence
linearizeCircle() {
double angleIncr = acos((c.radius - threshold) / c.radius);
PositionSequenceBuilder
builder = variableSized((Class
) p0.getClass());
double theta0 = angleInDirection(p0);
double theta1 = angleInDirection(p1);
builder.add(p0);
AddPointsBetweenPolarCoordinates(theta0, theta1, p0, p1, angleIncr, builder);
builder.add(p1);
//TODO -- I'm not sure whether this actually works if the circle defined by the 3 positions is in clockwise direction
// AddPointsBetweenPolarCoordinates(theta1, theta0 + 2 * Math.PI, p1, p0, angleIncr, builder, true);
AddPointsBetweenPolarCoordinates(theta1, theta0, p1, p0, angleIncr, builder);
builder.add(p0);
return builder.toPositionSequence();
}
/**
* Linearizes the arc segment defined by the three {@code Position}s specified in this instance's constructor.
* Compared to {@link #linearizeCircle()} this is not using normalized direction angels, as direction of arc
* will be lost in this case (Does not matter if working with a circle).
*
* @return a PositionSequence that approximates the arc segment
*/
public PositionSequence
linearize() {
double theta0 = angleInDirection(p0);
double theta1 = angleInDirection(p1);
double theta2 = angleInDirection(p2);
//we linearize by incrementing start angle theta by and increment.
// the following will always hold:
// radius = radius*cos(increment) + error
// we want error to be < threshold, so we can calculate increment
// notice that argument to acos is very close to 1 (if threshold is small, as we assume here)
// so angleIncrement is garuanteed to be positive and small
double angleIncr = acos((c.radius - threshold) / c.radius);
//TODO -- quick check if angles theta are closer together than angleIncr, then we don't need to
// linearize
//now we "walk" from theta, over theta1 to theta2 (or inversely)
PositionSequenceBuilder
builder = variableSized((Class
) p0.getClass());
switch (orientation) {
case Counterclockwise:
builder.add(p0);
AddPointsBetweenPolarCoordinates(theta0, theta1, p0, p1, angleIncr, builder);
builder.add(p1);
AddPointsBetweenPolarCoordinates(theta1, theta2, p1, p2, angleIncr, builder);
builder.add(p2);
return builder.toPositionSequence();
case Clockwise:
// If clockwise, we have to do it reverse
builder.add(p2);
AddPointsBetweenPolarCoordinates(theta2, theta1, p2, p1, angleIncr, builder);
builder.add(p1);
AddPointsBetweenPolarCoordinates(theta1, theta0, p1, p0, angleIncr, builder);
builder.add(p0);
return builder.toPositionSequence().reverse();
default:
builder.add(p0);
builder.add(p1);
builder.add(p2);
return builder.toPositionSequence();
}
}
// Adds points strictly between theta and theta1, using the specified angle-increment
// and interpolates the "higher dimensions" on the run
private void AddPointsBetweenPolarCoordinates(double theta, double theta1, P p, P p1, double maxAngleIncr,
PositionSequenceBuilder
builder) {
int dim = p.getCoordinateDimension();
// If working with arcs, we have to normalize theta1 that it always bigger than theta
// This is needed for computing steps only and only if arcs have not been normalized yet.
double normalizedTheta1 = theta1;
while(normalizedTheta1 < theta) {
normalizedTheta1 += 2.0 * Math.PI;
}
//first a number of steps and angleIncrement such that we go in equal increment steps from theta to theta1
int steps = (int) Math.ceil(Math.abs(normalizedTheta1 - theta) / maxAngleIncr);
double angleIncr = Math.abs(normalizedTheta1 - theta) / (double) steps;
//determine increments for Z and M dimensions:
double[] incr = new double[dim - 2];
for (int i = 0; i < incr.length; i++) {
incr[i] = (p1.getCoordinate(2 + i) - p.getCoordinate(2 + i)) / steps;
}
//now find direction (but only if working with normalized angles).
double sign = 1.0;
// we initialize the higher dimensions
double[] buf = new double[dim];
for (int i = 0; i < incr.length; i++) {
buf[2 + i] = p.getCoordinate(2 + i);
}
double a = theta;
for (int idx =0 ; idx < steps-1 ; idx++) {
a = a + sign * angleIncr;
//calculate x,y positions
buf[0] = c.x + c.radius * cos(a);
buf[1] = c.y + c.radius * sin(a);
//and interpolate
for (int i = 0; i < incr.length; i++) {
buf[2 + i] = buf[2 + i] + incr[i];
}
builder.add(buf);
}
}
//atan2 give the angular coordinate theta of the polar coordinates (r, theta)
//the angular coordinate ranges between -PI and PI.
private double angleInDirection(Position p) {
double x = (p.getCoordinate(0) - c.x);
double y = (p.getCoordinate(1) - c.y);
return atan2(y, x);
}
}