View Javadoc
1   package org.opentrafficsim.draw.road;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Font;
6   import java.awt.FontMetrics;
7   import java.awt.Graphics2D;
8   import java.awt.Rectangle;
9   import java.awt.font.TextAttribute;
10  import java.awt.geom.Ellipse2D;
11  import java.awt.geom.Path2D;
12  import java.awt.geom.RoundRectangle2D;
13  import java.awt.image.ImageObserver;
14  import java.util.Map;
15  
16  import org.opentrafficsim.base.geometry.OtsShape;
17  import org.opentrafficsim.draw.DrawLevel;
18  import org.opentrafficsim.draw.OtsRenderable;
19  import org.opentrafficsim.draw.road.PriorityAnimation.PriorityData;
20  
21  import nl.tudelft.simulation.naming.context.Contextualized;
22  
23  /**
24   * Animation of conflict priority (which is a link property).
25   * <p>
26   * Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
27   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
28   * </p>
29   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
30   */
31  public class PriorityAnimation extends OtsRenderable<PriorityData>
32  {
33  
34      /** Shadow. */
35      private static final Color SHADOW = new Color(0, 0, 0, 128);
36  
37      /** Shadow x translation. */
38      private static final double SHADOW_DX = 0.1;
39  
40      /** Shadow y translation. */
41      private static final double SHADOW_DY = 0.05;
42  
43      /**
44       * Constructor.
45       * @param source source.
46       * @param contextProvider contextualized.
47       */
48      public PriorityAnimation(final PriorityData source, final Contextualized contextProvider)
49      {
50          super(source, contextProvider);
51      }
52  
53      @Override
54      public boolean isRotate()
55      {
56          return false;
57      }
58  
59      @Override
60      public void paint(final Graphics2D graphics, final ImageObserver observer)
61      {
62          if (getSource().isNone())
63          {
64              return;
65          }
66          setRendering(graphics);
67          if (getSource().isAllStop() || getSource().isStop())
68          {
69              paintOctagon(graphics, 1.0, SHADOW, true, true);
70              paintOctagon(graphics, 1.0, Color.WHITE, true, false);
71              paintOctagon(graphics, 1.0, Color.BLACK, false, false);
72              paintOctagon(graphics, 0.868, new Color(230, 0, 0), true, false);
73              paintString(graphics, "STOP", Color.WHITE, 0.9f, getSource().isAllStop() ? -0.1f : 0.0f);
74              if (getSource().isAllStop())
75              {
76                  paintString(graphics, "ALL WAY", Color.WHITE, 0.4f, 0.45f);
77              }
78          }
79          else if (getSource().isBusStop())
80          {
81              graphics.setColor(SHADOW);
82              graphics.fill(new Ellipse2D.Double(-1.0 + SHADOW_DX, -1.0 + SHADOW_DY, 2.0, 2.0));
83              Color blue = new Color(20, 94, 169);
84              graphics.setColor(blue);
85              graphics.fill(new Ellipse2D.Double(-1.0, -1.0, 2.0, 2.0));
86              graphics.setColor(Color.WHITE);
87              graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
88              graphics.draw(new Ellipse2D.Double(-0.94, -0.94, 1.88, 1.88));
89              paintBus(graphics, blue);
90          }
91          else if (getSource().isPriority())
92          {
93              paintDiamond(graphics, 1.0, SHADOW, true, true);
94              paintDiamond(graphics, 1.0, Color.WHITE, true, false);
95              paintDiamond(graphics, 11.0 / 12.0, Color.BLACK, false, false);
96              paintDiamond(graphics, 11.0 / 18.0, new Color(255, 204, 0), true, false);
97          }
98          else if (getSource().isYield())
99          {
100             paintTriangle(graphics, 1.0, SHADOW, true, true);
101             paintTriangle(graphics, 1.0, new Color(230, 0, 0), true, false);
102             paintTriangle(graphics, 0.9, Color.WHITE, false, false);
103             paintTriangle(graphics, 0.55, Color.WHITE, true, false);
104         }
105         resetRendering(graphics);
106     }
107 
108     /**
109      * Paint octagon.
110      * @param graphics graphics.
111      * @param radius radius (half width).
112      * @param color color.
113      * @param fill fill (or draw line).
114      * @param shadow whether this is shadow.
115      */
116     private void paintOctagon(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
117             final boolean shadow)
118     {
119         double k = Math.tan(Math.PI / 8.0) * radius;
120         double dx = shadow ? SHADOW_DX : 0.0;
121         double dy = shadow ? SHADOW_DY : 0.0;
122         Path2D.Float path = new Path2D.Float();
123         path.moveTo(dx + radius, dy);
124         path.lineTo(dx + radius, dy + k);
125         path.lineTo(dx + k, dy + radius);
126         path.lineTo(dx - k, dy + radius);
127         path.lineTo(dx - radius, dy + k);
128         path.lineTo(dx - radius, dy - k);
129         path.lineTo(dx - k, dy - radius);
130         path.lineTo(dx + k, dy - radius);
131         path.lineTo(dx + radius, dy - k);
132         path.lineTo(dx + radius, dy);
133         graphics.setColor(color);
134         if (fill)
135         {
136             graphics.fill(path);
137         }
138         else
139         {
140             graphics.setStroke(new BasicStroke(0.02f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
141             graphics.draw(path);
142         }
143     }
144 
145     /**
146      * Paints a bus.
147      * @param graphics graphics.
148      * @param blue color used for blue background.
149      */
150     private void paintBus(final Graphics2D graphics, final Color blue)
151     {
152         // bus
153         Path2D.Double path = new Path2D.Double();
154         path.moveTo(0.77, -0.07);
155         path.lineTo(0.74, -0.36);
156         path.lineTo(-0.69, -0.36);
157         path.lineTo(-0.77, -0.07);
158         path.lineTo(-0.77, 0.22);
159         path.lineTo(0.43, 0.22);
160         path.lineTo(0.77, 0.17);
161         path.lineTo(0.77, -0.07);
162         graphics.fill(path);
163         // wheels
164         graphics.fill(new Ellipse2D.Double(-0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
165         graphics.fill(new Ellipse2D.Double(0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
166         graphics.setColor(blue);
167         graphics.setStroke(new BasicStroke(0.015f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
168         graphics.draw(new Ellipse2D.Double(-0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
169         graphics.draw(new Ellipse2D.Double(0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
170         // windows
171         graphics.setColor(blue);
172         path = new Path2D.Double();
173         path.moveTo(-0.52, -0.32);
174         path.lineTo(-0.66, -0.32);
175         path.lineTo(-0.73, -0.07);
176         path.lineTo(-0.52, -0.07);
177         path.lineTo(-0.52, -0.32);
178         graphics.fill(path);
179         for (double x : new double[] {-0.48, -0.23, 0.02, 0.27})
180         {
181             graphics.fill(new Rectangle.Double(x, -0.32, 0.21, 0.21));
182         }
183         path = new Path2D.Double();
184         path.moveTo(0.71, -0.32);
185         path.lineTo(0.52, -0.32);
186         path.lineTo(0.52, -0.11);
187         path.lineTo(0.73, -0.11);
188         path.lineTo(0.71, -0.32);
189         graphics.fill(path);
190     }
191 
192     /**
193      * Paint diamond.
194      * @param graphics graphics.
195      * @param radius radius (half width).
196      * @param color color.
197      * @param fill fill (or draw line).
198      * @param shadow whether this is shadow.
199      */
200     private void paintDiamond(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
201             final boolean shadow)
202     {
203         double dx = shadow ? SHADOW_DX : 0.0;
204         double dy = shadow ? SHADOW_DY : 0.0;
205         graphics.setColor(color);
206         if (fill)
207         {
208             Path2D.Float path = new Path2D.Float();
209             path.moveTo(dx + radius, dy);
210             path.lineTo(dx, dy + radius);
211             path.lineTo(dx - radius, dy);
212             path.lineTo(dx, dy - radius);
213             path.lineTo(dx + radius, dy);
214             graphics.fill(path);
215         }
216         else
217         {
218             // to assist rounded corners, we rotate by 1/8th circle and use RoundRectangle2D
219             graphics.rotate(Math.PI / 4);
220             graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
221             double r = radius / Math.sqrt(2.0); // diagonal vs. base
222             RoundRectangle2D.Double shape = new RoundRectangle2D.Double(-r, -r, 2.0 * r, 2.0 * r, 0.15 * r, 0.15 * r);
223             graphics.draw(shape);
224             graphics.rotate(-Math.PI / 4);
225         }
226     }
227 
228     /**
229      * Paint triangle.
230      * @param graphics graphics.
231      * @param radius radius (half width).
232      * @param color color.
233      * @param fill fill (or draw line).
234      * @param shadow whether this is shadow.
235      */
236     private void paintTriangle(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
237             final boolean shadow)
238     {
239         double k = radius * Math.sqrt(3.0) / 3.0;
240         double g = (radius * Math.sqrt(3.0)) - k;
241         double dx = shadow ? SHADOW_DX : 0.0;
242         double dy = shadow ? SHADOW_DY : 0.0;
243         Path2D.Float path = new Path2D.Float();
244         path.moveTo(dx + 0.0, dy - k);
245         path.lineTo(dx + -radius, dy - k);
246         path.lineTo(dx + 0.0, dy + g);
247         path.lineTo(dx + radius, dy - k);
248         path.lineTo(dx + 0.0, dy - k);
249         graphics.setColor(color);
250         if (fill)
251         {
252             graphics.fill(path);
253         }
254         else
255         {
256             graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
257             graphics.draw(path);
258         }
259     }
260 
261     /**
262      * Paint string.
263      * @param graphics graphics.
264      * @param text text.
265      * @param color color.
266      * @param fontSize font size.
267      * @param dy distance down from object location.
268      */
269     private void paintString(final Graphics2D graphics, final String text, final Color color, final float fontSize,
270             final float dy)
271     {
272         if (graphics.getTransform().getDeterminant() > 400000)
273         {
274             // TODO
275             /*
276              * If we are very zoomed in, the font gets huge on screen. FontMetrics somehow uses this actual image size in Java
277              * 11, and this gives a bug for fonts above a certain size. Dimensions become 0, and this does not recover after we
278              * zoom out again. The text never shows anymore. A later java version may not require skipping painting the font.
279              * See more at: https://bugs.openjdk.org/browse/JDK-8233097
280              */
281             return;
282         }
283         graphics.setColor(color);
284         int fontSizeMetrics = 100;
285         float factor = fontSize / fontSizeMetrics;
286         Font font = new Font("Arial", Font.BOLD, fontSizeMetrics).deriveFont(Map.of(TextAttribute.WIDTH, 0.67f));
287         graphics.setFont(font.deriveFont(fontSize));
288         FontMetrics metrics = graphics.getFontMetrics(font);
289         float w = metrics.stringWidth(text) * factor;
290         float d = metrics.getDescent() * factor;
291         float h = metrics.getHeight() * factor;
292         graphics.drawString(text, -w / 2.0f, dy + h / 2.0f - d);
293     }
294 
295     /**
296      * Data for priority animation.
297      * <p>
298      * Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
299      * <br>
300      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
301      * </p>
302      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
303      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
304      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
305      */
306     public interface PriorityData extends OtsShape
307     {
308 
309         @Override
310         default double getZ()
311         {
312             return DrawLevel.NODE.getZ();
313         }
314 
315         /**
316          * Returns whether the priority is all stop.
317          * @return whether the priority is all stop.
318          */
319         boolean isAllStop();
320 
321         /**
322          * Returns whether the priority is bus stop.
323          * @return whether the priority is bus stop.
324          */
325         boolean isBusStop();
326 
327         /**
328          * Returns whether the priority is none.
329          * @return whether the priority is none.
330          */
331         boolean isNone();
332 
333         /**
334          * Returns whether the priority is priority.
335          * @return whether the priority is priority.
336          */
337         boolean isPriority();
338 
339         /**
340          * Returns whether the priority is stop.
341          * @return whether the priority is stop.
342          */
343         boolean isStop();
344 
345         /**
346          * Returns whether the priority is yield.
347          * @return whether the priority is yield.
348          */
349         boolean isYield();
350     }
351 
352 }