StripeAnimation.java
package org.opentrafficsim.draw.road;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Path2D;
import java.awt.image.ImageObserver;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.djunits.value.vdouble.scalar.Length;
import org.djutils.draw.line.PolyLine2d;
import org.djutils.draw.line.Ray2d;
import org.djutils.draw.point.OrientedPoint2d;
import org.djutils.draw.point.Point2d;
import org.opentrafficsim.base.StripeElement;
import org.opentrafficsim.base.geometry.DirectionalPolyLine;
import org.opentrafficsim.base.geometry.OtsLine2d.FractionalFallback;
import org.opentrafficsim.draw.ClickableLineLocatable;
import org.opentrafficsim.draw.DrawLevel;
import org.opentrafficsim.draw.OtsRenderable;
import org.opentrafficsim.draw.PaintPolygons;
import org.opentrafficsim.draw.road.StripeAnimation.StripeData;
import nl.tudelft.simulation.naming.context.Contextualized;
/**
* Draw road stripes.
* <p>
* Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public class StripeAnimation extends OtsRenderable<StripeData>
{
/** */
private static final long serialVersionUID = 20141017L;
/** Drawable paths. */
private List<PaintData> paintDatas;
/** Offset that applied when paths were determined. */
private Length pathOffset;
/**
* @param source stripe data
* @param contextualized context provider
*/
public StripeAnimation(final StripeData source, final Contextualized contextualized)
{
super(source, contextualized);
}
/**
* Generate the points needed to draw the stripe pattern.
* @param stripe the stripe
* @return list of paint data
*/
private List<PaintData> makePaths(final StripeData stripe)
{
// TODO implement changing width along the length, when offset line with function for offset is supported
List<PaintData> paintData = new ArrayList<>();
double width = stripe.getWidth(Length.ZERO).si;
double edgeOffset = .5 * width;
for (StripeElement element : stripe.getElements())
{
double w = element.width().si;
List<Point2d> path = new ArrayList<>();
if (element.isContinuous())
{
stripe.getCenterLine().directionalOffsetLine(edgeOffset).getPoints().forEachRemaining(path::add);
stripe.getCenterLine().directionalOffsetLine(edgeOffset - w).reverse().getPoints().forEachRemaining(path::add);
}
else if (!element.isGap())
{
double[] dashes = element.dashes().getValuesSI();
path.addAll(makeDashes(stripe.getCenterLine().directionalOffsetLine(edgeOffset - .5 * w),
stripe.getReferenceLine(), w, stripe.getDashOffset().si, dashes));
}
edgeOffset -= w;
// can be empty for a gap element, or when no dash is within the length
if (!path.isEmpty())
{
paintData.add(new PaintData(PaintPolygons.getPaths(getSource().getLocation(), path), element.color()));
}
}
return paintData;
}
/**
* Generate the drawing commands for a dash pattern.
* @param centerLine the design line of the striped pattern
* @param referenceLine reference line to which dashes are applied
* @param width width of the stripes in meters
* @param startOffset shift the starting point in the pattern by this length in meters
* @param dashes one or more lengths of the dashes and the gaps between those dashes. The first value in <cite>dashes</cite>
* is the length of a gap. If the number of values in <cite>dashes</cite> is odd, the pattern repeats inverted
* (gaps become dashes, dashes become gaps).
* @return the coordinates of the dashes separated and terminated by a <cite>NEWPATH</cite> Coordinate
*/
private List<Point2d> makeDashes(final DirectionalPolyLine centerLine, final PolyLine2d referenceLine, final double width,
final double startOffset, final double[] dashes)
{
double period = 0;
for (double length : dashes)
{
if (length < 0)
{
throw new Error("Bad pattern - on or off length is < 0");
}
period += length;
}
if (period <= 0)
{
throw new Error("Bad pattern - repeat period length is 0");
}
// TODO link length when that is chosen
double referenceLength = referenceLine.getLength();
double position = -startOffset + dashes[0];
int phase = 1;
ArrayList<Point2d> result = new ArrayList<>();
boolean first = true;
boolean sameLine = centerLine.getPointList().equals(referenceLine.getPointList());
while (position < referenceLength)
{
double nextBoundary = position + dashes[phase++ % dashes.length];
if (nextBoundary > 0) // Skip this one; this entire dash lies within the startOffset
{
if (!first)
{
result.add(PaintPolygons.NEWPATH);
}
first = false;
if (position < 0)
{
position = 0; // Draw a partial dash, starting at 0 (begin of the center line)
}
double endPosition = nextBoundary;
if (endPosition > referenceLength)
{
endPosition = referenceLength; // Draw a partial dash, ending at length (end of the center line)
}
double fraction1 = position / referenceLength;
double fraction2 = endPosition / referenceLength;
if (!sameLine)
{
// project dash from reference line on the own center line, using fractional projection (i.e. pizza slices)
Ray2d p1 = referenceLine.getLocationFraction(fraction1);
Ray2d p2 = referenceLine.getLocationFraction(fraction2);
fraction1 = centerLine.projectFractional(p1.x, p1.y, FractionalFallback.ENDPOINT);
fraction2 = centerLine.projectFractional(p2.x, p2.y, FractionalFallback.ENDPOINT);
}
DirectionalPolyLine dashCenter = centerLine.extractFractional(fraction1, fraction2);
// create offsets on dash center line to add dash contour line
dashCenter.directionalOffsetLine(width / 2).getPoints().forEachRemaining(result::add);
dashCenter.directionalOffsetLine(-width / 2).reverse().getPoints().forEachRemaining(result::add);
}
position = nextBoundary + dashes[phase++ % dashes.length];
}
return result;
}
@Override
public final void paint(final Graphics2D graphics, final ImageObserver observer)
{
update();
if (this.paintDatas != null)
{
for (PaintData paintData : this.paintDatas)
{
setRendering(graphics);
graphics.setStroke(new BasicStroke(2.0f));
PaintPolygons.paintPaths(graphics, paintData.color(), paintData.path(), true);
resetRendering(graphics);
}
}
}
/**
* Updates paths to draw when new offset applies.
*/
private void update()
{
if (!getSource().getDashOffset().equals(this.pathOffset))
{
this.paintDatas = makePaths(getSource());
this.pathOffset = getSource().getDashOffset();
}
}
@Override
public final String toString()
{
return "StripeAnimation [source = " + getSource().toString() + ", paintDatas=" + this.paintDatas + "]";
}
/**
* StripeData provides the information required to draw a stripe.
* <p>
* Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public interface StripeData extends ClickableLineLocatable
{
@Override
OrientedPoint2d getLocation();
/**
* Returns the center line in world coordinates, with directions of end-points.
* @return the center line in world coordinates, with directions of end-points
*/
DirectionalPolyLine getCenterLine();
/**
* Returns the line along which dashes are applied. At these fractions, parts of the centerline are taken.
* @return line along which dashes are applied
*/
PolyLine2d getReferenceLine();
/**
* Returns the stripe elements.
* @return stripe elements
*/
List<StripeElement> getElements();
/**
* Return dash offset.
* @return dash offset
*/
Length getDashOffset();
/**
* Returns the line width.
* @param position where to obtain width
* @return line width
*/
Length getWidth(Length position);
@Override
default double getZ()
{
return DrawLevel.MARKING.getZ();
}
}
/**
* Paint data.
* @param path path
* @param color color
*/
private record PaintData(Set<Path2D.Float> path, Color color)
{
}
}