package org.opentrafficsim.draw.road; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.ImageObserver; import java.io.Serializable; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.naming.NamingException; import org.djutils.draw.line.PolyLine2d; import org.djutils.draw.line.PolyLine3d; import org.djutils.draw.line.Polygon3d; import org.djutils.draw.point.Point3d; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.linearref.LengthIndexedLine; import org.locationtech.jts.operation.buffer.BufferParameters; import org.opentrafficsim.draw.core.ClonableRenderable2DInterface; import org.opentrafficsim.draw.core.PaintPolygons; import org.opentrafficsim.road.network.lane.Stripe; import nl.tudelft.simulation.dsol.animation.D2.Renderable2D; import nl.tudelft.simulation.dsol.simulators.SimulatorInterface; /** * Draw road stripes. *

* Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License. *

* $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $, * initial version Oct 17, 2014
* @author Alexander Verbraeck */ public class StripeAnimation extends Renderable2D implements ClonableRenderable2DInterface, Serializable { /** */ private static final long serialVersionUID = 20141017L; /** The line type. */ private final TYPE type; /** The points for the outline of the Stripe. */ private final List line; /** Precision of buffer operations. */ private static final int QUADRANTSEGMENTS = 8; /** * Generate the drawing commands for a dash pattern. * @param center LengthIndexedLine; the design line of the striped pattern * @param width double; width of the stripes in meters * @param startOffset double; shift the starting point in the pattern by this length in meters * @param onOffLengths double[]; one or more lengths of the dashes and the gaps between those dashes. If the number of * values in onOffLengths is odd, the pattern repeats inverted. The first value in * onOffLengths is the length of a dash. * @return List<Polygon3d>; the coordinates of the dashes separated and terminated by a NEWPATH * Coordinate */ // TODO startOffset does not work if a dash falls inside of it (so below the offset is 2.99m, rather than 3m) private List makeDashes(final LengthIndexedLine center, final double width, final double startOffset, final double[] onOffLengths) { double period = 0; for (double length : onOffLengths) { 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"); } double length = center.getEndIndex(); double position = -startOffset; int phase = 0; ArrayList result = new ArrayList<>(); while (position < length) { double nextBoundary = position + onOffLengths[phase++ % onOffLengths.length]; if (nextBoundary > 0) // Skip this one; this entire dash lies within the startOffset { if (position < 0) { position = 0; // Draw a partial dash, starting at 0 (begin of the center line) } double endPosition = nextBoundary; if (endPosition > length) { endPosition = length; // Draw a partial dash, ending at length (end of the center line) } Coordinate[] oneDash = center.extractLine(position, endPosition) .buffer(width / 2, QUADRANTSEGMENTS, BufferParameters.CAP_FLAT).getCoordinates(); Point3d[] oneDashPoints = new Point3d[oneDash.length]; for (int i = 0; i < oneDash.length; i++) { if (Double.isNaN(oneDash[i].z)) { oneDash[i].z = 0; } oneDashPoints[i] = new Point3d(oneDash[i].x, oneDash[i].y, oneDash[i].z); } result.add(new Polygon3d(oneDashPoints)); } position = nextBoundary + onOffLengths[phase++ % onOffLengths.length]; } return result; } /** * Construct a LineString from a PolyLine3D. * @param polyLine PolyLine3d; the line * @return a LineString corresponding to the line */ public final LineString getLineString(final PolyLine3d polyLine) { GeometryFactory factory = new GeometryFactory(); Coordinate[] coordinates = new Coordinate[polyLine.size()]; int nextIndex = 0; for (Iterator iterator = polyLine.getPoints(); iterator.hasNext();) { Point3d point = iterator.next(); coordinates[nextIndex++] = new Coordinate(point.x, point.y, point.z); } CoordinateSequence cs = factory.getCoordinateSequenceFactory().create(coordinates); return new LineString(cs, factory); } /** * Really simple minded offsetLine method for 3d coordinates. * @param reference PolyLine3d; the reference line * @param offset double; the offset * @return PolyLine3d; the offset line */ PolyLine3d offsetLine(final PolyLine3d reference, final double offset) { PolyLine2d projected = reference.project().offsetLine(offset); List points = new ArrayList<>(); double fromZ = reference.getFirst().z; double toZ = reference.getLast().z; points.add(new Point3d(projected.getFirst(), fromZ)); // Linear interpolate z (very wrong, but not too bad for stripes) for (int index = 1; index < projected.size() - 1; index++) { points.add(new Point3d(projected.get(index), fromZ + (toZ - fromZ) * index / projected.size())); } points.add(new Point3d(projected.getLast(), reference.getLast().z)); return new PolyLine3d(true, points); } /** * Generate the points needed to draw the stripe pattern. * @param stripe Stripe; the stripe * @param stripeType TYPE; the stripe type * @return List<Polygon3d>; a list of polygons * @throws NamingException when type is not supported */ private List makePoints(final Stripe stripe, final TYPE stripeType) throws NamingException { switch (this.type) { case DASHED:// ¦ - Draw a 3-9 dash pattern on the center line return makeDashes(new LengthIndexedLine(getLineString(stripe.getCenterLine())), 0.2, 3.0, new double[] {3, 9}); case BLOCK:// : - Draw a 1-3 dash pattern on the center line return makeDashes(new LengthIndexedLine(getLineString(stripe.getCenterLine())), 0.45, 1.0, new double[] {1, 3}); case DOUBLE:// || - Draw two solid lines { // TODO rewrite not using the Geometry.org.locationtech buffer operation PolyLine3d centerLine = stripe.getCenterLine(); Coordinate[] leftLine = getLineString(offsetLine(centerLine, 0.2)) .buffer(0.1, QUADRANTSEGMENTS, BufferParameters.CAP_FLAT).getCoordinates(); Coordinate[] rightLine = getLineString(offsetLine(centerLine, -0.2)) .buffer(0.1, QUADRANTSEGMENTS, BufferParameters.CAP_FLAT).getCoordinates(); List polygonPoints = new ArrayList<>(leftLine.length + rightLine.length); for (int i = 0; i < leftLine.length; i++) { polygonPoints.add(new Point3d(leftLine[i].x, leftLine[i].y, leftLine[i].z)); } for (int i = 0; i < rightLine.length; i++) { polygonPoints.add(new Point3d(rightLine[i].x, rightLine[i].y, rightLine[i].z)); } List result = new ArrayList<>(1); result.add(new Polygon3d(polygonPoints)); return result; } case LEFTONLY: // |¦ - Draw left solid, right 3-9 dashed { PolyLine3d centerLine = stripe.getCenterLine(); Geometry rightDesignLine = getLineString(offsetLine(centerLine, -0.2)); List result = makeDashes(new LengthIndexedLine(rightDesignLine), 0.2, 0.0, new double[] {3, 9}); Coordinate[] leftCoordinates = getLineString(offsetLine(centerLine, 0.2)) .buffer(0.1, QUADRANTSEGMENTS, BufferParameters.CAP_FLAT).getCoordinates(); Point3d[] polygonPoints = new Point3d[leftCoordinates.length]; for (int i = 0; i < leftCoordinates.length; i++) { polygonPoints[i] = new Point3d(leftCoordinates[i].x, leftCoordinates[i].y, leftCoordinates[i].z); } result.add(new Polygon3d(polygonPoints)); return result; } case RIGHTONLY: // ¦| - Draw left 3-9 dashed, right solid { PolyLine3d centerLine = stripe.getCenterLine(); Geometry leftDesignLine = getLineString(offsetLine(centerLine, 0.2)); List result = makeDashes(new LengthIndexedLine(leftDesignLine), 0.2, 0.0, new double[] {3, 9}); Coordinate[] rightCoordinates = getLineString(offsetLine(centerLine, -0.2)) .buffer(0.1, QUADRANTSEGMENTS, BufferParameters.CAP_FLAT).getCoordinates(); Point3d[] polygonPoints = new Point3d[rightCoordinates.length]; for (int i = 0; i < rightCoordinates.length; i++) { polygonPoints[i] = new Point3d(rightCoordinates[i].x, rightCoordinates[i].y, rightCoordinates[i].z); } result.add(new Polygon3d(polygonPoints)); return result; } case SOLID:// | - Draw single solid line. This (regretfully) involves copying everything twice... { ArrayList polygonPoints = new ArrayList<>(); for (Iterator iterator = stripe.getContour().getPoints(); iterator.hasNext();) { polygonPoints.add(iterator.next()); } List result = new ArrayList<>(1); result.add(new Polygon3d(polygonPoints)); return result; } default: throw new NamingException("Unsupported stripe type: " + stripeType); } } /** * @param source Stripe; s * @param simulator SimulatorInterface.TimeDoubleUnit; s * @param type TYPE; t * @throws NamingException ne * @throws RemoteException on communication failure */ public StripeAnimation(final Stripe source, final SimulatorInterface.TimeDoubleUnit simulator, final TYPE type) throws NamingException, RemoteException { super(source, simulator); this.type = type; List list = makePoints(source, type); if (!list.isEmpty()) { this.line = list; } else { // no dash within length this.line = null; } } /** {@inheritDoc} */ @Override public final void paint(final Graphics2D graphics, final ImageObserver observer) { if (this.line != null) { graphics.setStroke(new BasicStroke(2.0f)); PaintPolygons.paintMultiPolygon(graphics, Color.WHITE, getSource().getLocation(), this.line, true); } } /** * Stripe type. *

* Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands.
* All rights reserved.
* BSD-style license. See OpenTrafficSim License. */ public enum TYPE { /** Single solid line. */ SOLID, /** Line |¦ allow to go to left, but not to right. */ LEFTONLY, /** Line ¦| allow to go to right, but not to left. */ RIGHTONLY, /** Dashes ¦ allow to cross in both directions. */ DASHED, /** Double solid line ||, don't cross. */ DOUBLE, /** Block : allow to cross in both directions. */ BLOCK } /** {@inheritDoc} */ @Override public ClonableRenderable2DInterface clone(final Stripe newSource, final SimulatorInterface.TimeDoubleUnit newSimulator) throws NamingException, RemoteException { return new StripeAnimation(newSource, newSimulator, this.type); } /** {@inheritDoc} */ @Override public final String toString() { return "StripeAnimation [source = " + getSource().toString() + ", type=" + this.type + ", line=" + this.line + "]"; } }