View Javadoc
1   package org.opentrafficsim.draw.road;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Graphics2D;
6   import java.awt.geom.Path2D;
7   import java.awt.image.ImageObserver;
8   import java.util.ArrayList;
9   import java.util.List;
10  import java.util.Set;
11  
12  import org.djunits.value.vdouble.scalar.Length;
13  import org.djutils.draw.line.PolyLine2d;
14  import org.djutils.draw.point.DirectedPoint2d;
15  import org.djutils.draw.point.Point2d;
16  import org.opentrafficsim.base.StripeElement;
17  import org.opentrafficsim.base.geometry.DirectionalPolyLine;
18  import org.opentrafficsim.base.geometry.OtsLine2d.FractionalFallback;
19  import org.opentrafficsim.draw.DrawLevel;
20  import org.opentrafficsim.draw.LineLocatable;
21  import org.opentrafficsim.draw.OtsRenderable;
22  import org.opentrafficsim.draw.PaintPolygons;
23  import org.opentrafficsim.draw.road.StripeAnimation.StripeData;
24  
25  import nl.tudelft.simulation.naming.context.Contextualized;
26  
27  /**
28   * Draw road stripes.
29   * <p>
30   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
31   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
32   * </p>
33   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
34   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
35   */
36  public class StripeAnimation extends OtsRenderable<StripeData>
37  {
38      /** Drawable paths. */
39      private List<PaintData> paintDatas;
40  
41      /** Offset that applied when paths were determined. */
42      private Length pathOffset;
43  
44      /**
45       * Constructor.
46       * @param source stripe data
47       * @param contextualized context provider
48       */
49      public StripeAnimation(final StripeData source, final Contextualized contextualized)
50      {
51          super(source, contextualized);
52      }
53  
54      /**
55       * Generate the points needed to draw the stripe pattern.
56       * @param stripe the stripe
57       * @return list of paint data
58       */
59      private List<PaintData> makePaths(final StripeData stripe)
60      {
61          // TODO implement changing width along the length, when offset line with function for offset is supported
62          List<PaintData> paintData = new ArrayList<>();
63          double width = stripe.getWidth(Length.ZERO).si;
64          double edgeOffset = .5 * width;
65          for (StripeElement element : stripe.getElements())
66          {
67              double w = element.width().si;
68              List<Point2d> path = new ArrayList<>();
69              if (element.isContinuous())
70              {
71                  stripe.getCenterLine().directionalOffsetLine(edgeOffset).iterator().forEachRemaining(path::add);
72                  stripe.getCenterLine().directionalOffsetLine(edgeOffset - w).reverse().iterator().forEachRemaining(path::add);
73              }
74              else if (!element.isGap())
75              {
76                  double[] dashes = element.dashes().getValuesSI();
77                  path.addAll(makeDashes(stripe.getCenterLine().directionalOffsetLine(edgeOffset - .5 * w),
78                          stripe.getReferenceLine(), w, stripe.getDashOffset().si, dashes));
79              }
80              edgeOffset -= w;
81              // can be empty for a gap element, or when no dash is within the length
82              if (!path.isEmpty())
83              {
84                  paintData.add(new PaintData(PaintPolygons.getPaths(getSource().getLocation(), path), element.color()));
85              }
86          }
87          return paintData;
88      }
89  
90      /**
91       * Generate the drawing commands for a dash pattern.
92       * @param centerLine the design line of the striped pattern
93       * @param referenceLine reference line to which dashes are applied
94       * @param width width of the stripes in meters
95       * @param startOffset shift the starting point in the pattern by this length in meters
96       * @param dashes one or more lengths of the dashes and the gaps between those dashes. The first value in <cite>dashes</cite>
97       *            is the length of a gap. If the number of values in <cite>dashes</cite> is odd, the pattern repeats inverted
98       *            (gaps become dashes, dashes become gaps).
99       * @return the coordinates of the dashes separated and terminated by a <cite>NEWPATH</cite> Coordinate
100      */
101     private List<Point2d> makeDashes(final DirectionalPolyLine centerLine, final PolyLine2d referenceLine, final double width,
102             final double startOffset, final double[] dashes)
103     {
104         double period = 0;
105         for (double length : dashes)
106         {
107             if (length < 0)
108             {
109                 throw new Error("Bad pattern - on or off length is < 0");
110             }
111             period += length;
112         }
113         if (period <= 0)
114         {
115             throw new Error("Bad pattern - repeat period length is 0");
116         }
117         // TODO link length when that is chosen
118         double referenceLength = referenceLine.getLength();
119         double position = -startOffset + dashes[0];
120         int phase = 1;
121         ArrayList<Point2d> result = new ArrayList<>();
122         boolean first = true;
123         boolean sameLine = centerLine.getPointList().equals(referenceLine.getPointList());
124         while (position < referenceLength)
125         {
126             double nextBoundary = position + dashes[phase++ % dashes.length];
127             if (nextBoundary > 0) // Skip this one; this entire dash lies within the startOffset
128             {
129                 if (!first)
130                 {
131                     result.add(PaintPolygons.NEWPATH);
132                 }
133                 first = false;
134                 if (position < 0)
135                 {
136                     position = 0; // Draw a partial dash, starting at 0 (begin of the center line)
137                 }
138                 double endPosition = nextBoundary;
139                 if (endPosition > referenceLength)
140                 {
141                     endPosition = referenceLength; // Draw a partial dash, ending at length (end of the center line)
142                 }
143 
144                 double fraction1 = position / referenceLength;
145                 double fraction2 = endPosition / referenceLength;
146                 if (!sameLine)
147                 {
148                     // project dash from reference line on the own center line, using fractional projection (i.e. pizza slices)
149                     DirectedPoint2d p1 = referenceLine.getLocationFraction(fraction1);
150                     DirectedPoint2d p2 = referenceLine.getLocationFraction(fraction2);
151                     fraction1 = centerLine.projectFractional(p1.x, p1.y, FractionalFallback.ENDPOINT);
152                     fraction2 = centerLine.projectFractional(p2.x, p2.y, FractionalFallback.ENDPOINT);
153                 }
154                 DirectionalPolyLine dashCenter = centerLine.extractFractional(fraction1, fraction2);
155 
156                 // create offsets on dash center line to add dash contour line
157                 dashCenter.directionalOffsetLine(width / 2).iterator().forEachRemaining(result::add);
158                 dashCenter.directionalOffsetLine(-width / 2).reverse().iterator().forEachRemaining(result::add);
159             }
160             position = nextBoundary + dashes[phase++ % dashes.length];
161         }
162         return result;
163     }
164 
165     @Override
166     public final void paint(final Graphics2D graphics, final ImageObserver observer)
167     {
168         update();
169         if (this.paintDatas != null)
170         {
171             for (PaintData paintData : this.paintDatas)
172             {
173                 setRendering(graphics);
174                 graphics.setStroke(new BasicStroke(2.0f));
175                 PaintPolygons.paintPaths(graphics, paintData.color(), paintData.path(), true);
176                 resetRendering(graphics);
177             }
178         }
179     }
180 
181     /**
182      * Updates paths to draw when new offset applies.
183      */
184     private void update()
185     {
186         if (!getSource().getDashOffset().equals(this.pathOffset))
187         {
188             this.paintDatas = makePaths(getSource());
189             this.pathOffset = getSource().getDashOffset();
190         }
191     }
192 
193     @Override
194     public final String toString()
195     {
196         return "StripeAnimation [source = " + getSource().toString() + ", paintDatas=" + this.paintDatas + "]";
197     }
198 
199     /**
200      * StripeData provides the information required to draw a stripe.
201      * <p>
202      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
203      * <br>
204      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
205      * </p>
206      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
207      */
208     public interface StripeData extends LineLocatable
209     {
210         @Override
211         DirectedPoint2d getLocation();
212 
213         /**
214          * Returns the center line in world coordinates, with directions of end-points.
215          * @return the center line in world coordinates, with directions of end-points
216          */
217         DirectionalPolyLine getCenterLine();
218 
219         /**
220          * Returns the line along which dashes are applied. At these fractions, parts of the centerline are taken.
221          * @return line along which dashes are applied
222          */
223         PolyLine2d getReferenceLine();
224 
225         /**
226          * Returns the stripe elements.
227          * @return stripe elements
228          */
229         List<StripeElement> getElements();
230 
231         /**
232          * Return dash offset.
233          * @return dash offset
234          */
235         Length getDashOffset();
236 
237         /**
238          * Returns the line width.
239          * @param position where to obtain width
240          * @return line width
241          */
242         Length getWidth(Length position);
243 
244         @Override
245         default double getZ()
246         {
247             return DrawLevel.MARKING.getZ();
248         }
249     }
250 
251     /**
252      * Paint data.
253      * @param path path
254      * @param color color
255      */
256     private record PaintData(Set<Path2D.Float> path, Color color)
257     {
258     }
259 }