View Javadoc
1   package org.opentrafficsim.animation.gtu;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Graphics2D;
6   import java.awt.RenderingHints;
7   import java.awt.geom.AffineTransform;
8   import java.awt.geom.Ellipse2D;
9   import java.awt.geom.Line2D;
10  import java.awt.image.ImageObserver;
11  import java.util.Set;
12  
13  import org.djunits.value.vdouble.scalar.Angle;
14  import org.djunits.value.vdouble.scalar.Duration;
15  import org.djunits.value.vdouble.scalar.Length;
16  import org.djutils.exceptions.Throw;
17  import org.opentrafficsim.animation.BoundsPaintScale;
18  import org.opentrafficsim.animation.Colors;
19  import org.opentrafficsim.animation.OtsRenderable;
20  import org.opentrafficsim.animation.gtu.PerceptionAnimation.PerceptionData;
21  import org.opentrafficsim.animation.gtu.PerceptionAnimation.PerceptionData.ChannelData;
22  import org.opentrafficsim.animation.gtu.PerceptionAnimation.PerceptionData.ChannelRadius;
23  import org.opentrafficsim.base.geometry.OtsShape;
24  
25  import nl.tudelft.simulation.naming.context.Contextualized;
26  
27  /**
28   * Draws circles around a GTU indicating the level of attention and perception delay.
29   * <p>
30   * Copyright (c) 2026-2026 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 Wouter Schakel
34   */
35  public class PerceptionAnimation extends OtsRenderable<PerceptionData>
36  {
37  
38      /** Maximum radius of attention circles. */
39      private static final double MAX_RADIUS = 1.425; // 1.5 minus half of LINE_WIDTH
40  
41      /** Radius around GTU along which the regular attention circles are placed. */
42      private static final double CENTER_RADIUS = 3.0;
43  
44      /** Radius around GTU along which the attention circles of objects are placed. */
45      private static final double CENTER_RADIUS_OBJECTS = 6.0;
46  
47      /** Line width around circle. */
48      private static final float LINE_WIDTH = 0.15f;
49  
50      /** Color scale for perception delay. */
51      private static final BoundsPaintScale SCALE =
52              new BoundsPaintScale(new double[] {0.0, 0.25, 0.5, 0.75, 1.0}, Colors.GREEN_RED_DARK);
53  
54      /**
55       * Constructor.
56       * @param perceptionData perception data
57       * @param contextualized contextualized, e.g. simulator
58       */
59      public PerceptionAnimation(final PerceptionData perceptionData, final Contextualized contextualized)
60      {
61          super(perceptionData, contextualized);
62      }
63  
64      @Override
65      public void paint(final Graphics2D graphics, final ImageObserver observer)
66      {
67          if (getSource().getChannels() == null || getSource().getChannels().isEmpty())
68          {
69              return;
70          }
71  
72          AffineTransform transform = graphics.getTransform();
73          graphics.setStroke(new BasicStroke(LINE_WIDTH));
74          graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
75  
76          boolean hasInVehicle = getSource().getChannels().stream().anyMatch((cd) -> cd.radius.equals(ChannelRadius.ZERO));
77          double dx = getSource().getDeltaCenterX().si;
78          for (ChannelData channelData : getSource().getChannels())
79          {
80              double radius = channelData.radius().equals(ChannelRadius.NEAR) ? CENTER_RADIUS
81                      : (channelData.radius().equals(ChannelRadius.FAR) ? CENTER_RADIUS_OBJECTS : 0.0);
82              boolean drawLine = (channelData.radius().equals(ChannelRadius.NEAR) && !hasInVehicle)
83                      || channelData.radius().equals(ChannelRadius.FAR);
84              drawAttentionCircle(graphics, dx, channelData.attention(), channelData.perceptionDelay().si, channelData.angle().si,
85                      radius, drawLine);
86              graphics.setTransform(transform);
87          }
88  
89      }
90  
91      /**
92       * Draws attention circle.
93       * @param graphics graphics
94       * @param dx longitudinal shift
95       * @param attention attention level
96       * @param perceptionDelay perception delay
97       * @param angle angle to draw circle at relative to GTU
98       * @param radius center circle radius around GTU
99       * @param drawLine whether to draw the line
100      */
101     private static void drawAttentionCircle(final Graphics2D graphics, final double dx, final double attention,
102             final double perceptionDelay, final double angle, final double radius, final boolean drawLine)
103     {
104         // on center of GTU
105         graphics.translate(dx, 0.0);
106         graphics.rotate(-angle, 0.0, 0.0);
107 
108         // connecting line
109         if (drawLine)
110         {
111             graphics.setColor(Color.GRAY);
112             graphics.draw(new Line2D.Double(0.0, 0.0, radius - MAX_RADIUS - LINE_WIDTH, 0.0));
113         }
114 
115         // transparent background fill
116         Color color = SCALE.getPaint(Math.min(1.0, perceptionDelay));
117         graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 48));
118         graphics.fill(new Ellipse2D.Double(radius - MAX_RADIUS, -MAX_RADIUS, 2.0 * MAX_RADIUS, 2.0 * MAX_RADIUS));
119 
120         // non-transparent attention fill
121         graphics.setColor(color);
122         double r = Math.sqrt(attention);
123         graphics.fill(
124                 new Ellipse2D.Double(radius - r * MAX_RADIUS, -r * MAX_RADIUS, 2.0 * r * MAX_RADIUS, 2.0 * r * MAX_RADIUS));
125 
126         // edge of circle
127         graphics.setColor(Color.GRAY);
128         float lineWidth = LINE_WIDTH - 0.02f; // prevent tiny edges between fill and border
129         graphics.draw(new Ellipse2D.Double(radius - MAX_RADIUS - .5 * lineWidth, -MAX_RADIUS - .5 * lineWidth,
130                 2.0 * MAX_RADIUS + lineWidth, 2.0 * MAX_RADIUS + lineWidth));
131     }
132 
133     /**
134      * Perception animation data.
135      */
136     public interface PerceptionData extends OtsShape
137     {
138 
139         /**
140          * Returns the forward delta from the reference to the center of the channel animation. This value may be negative.
141          * @return forward delta from the reference to the center of the channel animation
142          */
143         Length getDeltaCenterX();
144 
145         /**
146          * Returns the channels.
147          * @return channels
148          */
149         Set<ChannelData> getChannels();
150 
151         /**
152          * Data required per channel.
153          * @param angle angle relative to GTU
154          * @param radius radius from GTU reference
155          * @param attention attention level
156          * @param perceptionDelay perception delay
157          */
158         record ChannelData(Angle angle, ChannelRadius radius, double attention, Duration perceptionDelay)
159         {
160             /**
161              * Constructor.
162              * @param angle angle relative to GTU
163              * @param radius radius from GTU reference
164              * @param attention attention level
165              * @param perceptionDelay perception delay
166              */
167             public ChannelData
168             {
169                 Throw.whenNull(angle, "angle");
170                 Throw.whenNull(radius, "radius");
171                 Throw.whenNull(attention, "attention");
172                 Throw.whenNull(perceptionDelay, "perceptionDelay");
173             }
174         }
175 
176         /**
177          * Radius type.
178          */
179         enum ChannelRadius
180         {
181             /** Drawn with 0 radius. */
182             ZERO,
183 
184             /** Drawn with small radius. */
185             NEAR,
186 
187             /** Drawn with large radius. */
188             FAR;
189         }
190 
191     }
192 
193 }