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