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