Flattener.java
package org.djutils.draw.curve;
import java.util.NavigableMap;
import java.util.Set;
import org.djutils.draw.line.PolyLine;
import org.djutils.draw.point.Point;
import org.djutils.exceptions.Throw;
/**
* Flattens a Curve in to a PolyLine.
* <p>
* Copyright (c) 2024-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
* distributed under a three-clause BSD-style license, which can be found at
* <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
* </p>
* @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
* @param <F> the Flattener type
* @param <C> the Curve type
* @param <PL> the PolyLine type
* @param <P> the Point type
* @param <DIR> the Direction type
*/
public interface Flattener<F extends Flattener<F, C, PL, P, DIR>, C extends Curve<?, ?, P, F, PL>,
PL extends PolyLine<?, P, ?, ?, ?>, P extends Point<P>, DIR>
{
/**
* Load one knot in the map of fractions and points.
* @param map the map
* @param knot the fraction where the knot occurs
* @param curve the curve that can compute the point
*/
default void loadKnot(final NavigableMap<Double, P> map, final double knot, final C curve)
{
Throw.when(knot < 0.0 || knot > 1.0, IllegalArgumentException.class, "Knots must all be between 0.0 and 1.0, (got %f)",
knot);
if (map.containsKey(knot))
{
return;
}
map.put(knot, curve.getPoint(knot));
}
/**
* Load the knots into the navigable map (including the start point and the end point).
* @param map the navigable map
* @param curve the curve that can yield a Point for every knot position
*/
default void loadKnots(final NavigableMap<Double, P> map, final C curve)
{
map.put(0.0, curve.getPoint(0.0));
Set<Double> knots = curve.getKnots();
if (null != knots)
{
for (double knot : knots)
{
loadKnot(map, knot, curve);
}
}
map.put(1.0, curve.getPoint(1.0));
}
/**
* Report if the medianPoint is too far from the line segment from prevPoint to nextPoint.
* @param medianPoint P
* @param prevPoint P
* @param nextPoint P
* @param maxDeviation double
* @return <code>true</code> if the <code>medianPoint</code> is too far from the line segment; <code>false</code>
* if the <code>medianPoint</code> is close enough to the line segment
*/
default boolean checkPositionError(final P medianPoint, final P prevPoint, final P nextPoint, final double maxDeviation)
{
P projectedPoint = medianPoint.closestPointOnSegment(prevPoint, nextPoint);
double positionError = medianPoint.distance(projectedPoint);
return positionError > maxDeviation;
}
/**
* Check for a direction change of more than 90 degrees. If that happens, the MaxDeviation flattener must zoom in closer.
* @param prevDirection the direction at the preceding (already added) point
* @param nextDirection the direction at the succeeding (already added) point
* @return <code>true</code> if the curve changes direction by more than 90 degrees; <code>false</code> if the
* curve does not change direction by more than 90 degrees
*/
boolean checkLoopBack(DIR prevDirection, DIR nextDirection);
/**
* Check direction difference at the start and end of a segment.
* @param segmentDirection direction of the segment
* @param curveDirectionAtStart direction of the curve at the start of the segment
* @param curveDirectionAtEnd direction of the curve at the end of the segment
* @param maxDirectionDeviation maximum permitted direction difference
* @return <code>true</code> if the direction difference at the start and the end of the segment is smaller than
* <code>maxDirectionDeviation</code>; <code>false</code> if the direction difference at the start, or the end of
* the segment equals or exceeds <code>maxDirectionDeviation</code>
*/
boolean checkDirectionError(DIR segmentDirection, DIR curveDirectionAtStart, DIR curveDirectionAtEnd,
double maxDirectionDeviation);
/**
* Check for an inflection point by computing additional points at one quarter and three quarters. If these are on opposite
* sides of the curve2d from prevPoint to nextPoint; there must be an inflection point. This default implementation is
* <b>only for the 2d case</b>.
* https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line
* @param curve Curve2d
* @param prevT t of preceding inserted point
* @param medianT t of point currently considered for insertion
* @param nextT t of following inserted point
* @param prevPoint point on <code>curve</code> at <code>prevT</code>
* @param nextPoint point on <code>curve</code> at <code>nextT</code>
* @return <code>true</code> if there is an inflection point between <code>prevT</code> and <code>nextT</code>;
* <code>false</code> if there is no inflection point between <code>prevT</code> and <code>nextT</code>
*/
default boolean checkInflectionPoint(final FlattableCurve<P, DIR> curve, final double prevT, final double medianT,
final double nextT, final P prevPoint, final P nextPoint)
{
P oneQuarter = curve.getPoint((prevT + medianT) / 2);
int sign1 = (int) Math.signum((nextPoint.getX() - prevPoint.getX()) * (oneQuarter.getY() - prevPoint.getY())
- (nextPoint.getY() - prevPoint.getY()) * (oneQuarter.getX() - prevPoint.getX()));
P threeQuarter = curve.getPoint((nextT + medianT) / 2);
int sign2 = (int) Math.signum((nextPoint.getX() - prevPoint.getX()) * (threeQuarter.getY() - prevPoint.getY())
- (nextPoint.getY() - prevPoint.getY()) * (threeQuarter.getX() - prevPoint.getX()));
return sign1 != sign2;
}
/**
* Interface for getPoint and getDirection that hide whether or not an offset is applied.
* @param <P> the Point type
* @param <DIR> the Direction type
*/
interface FlattableCurve<P, DIR>
{
/**
* Get a Point for some fraction along the Curve.
* @param fraction the fraction to use
* @return the point at the <code>fraction</code>
*/
P getPoint(double fraction);
/**
* Get the direction at some fraction along the Curve.
* @param fraction the fraction to check
* @return the direction at the <code>fraction</code>
*/
DIR getDirection(double fraction);
}
}