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