View Javadoc
1   package org.opentrafficsim.animation;
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.BufferedImage;
11  import java.awt.image.ImageObserver;
12  import java.io.File;
13  import java.io.IOException;
14  import java.util.Optional;
15  import java.util.Set;
16  
17  import javax.imageio.ImageIO;
18  
19  import org.djunits.value.vdouble.scalar.Duration;
20  import org.djutils.draw.line.Polygon2d;
21  import org.djutils.draw.point.DirectedPoint2d;
22  import org.djutils.draw.point.Point2d;
23  import org.djutils.math.AngleUtil;
24  import org.opentrafficsim.animation.PerceptionAnimation.ChannelAttention;
25  import org.opentrafficsim.base.geometry.OtsShape;
26  import org.opentrafficsim.draw.BoundsPaintScale;
27  import org.opentrafficsim.draw.Colors;
28  import org.opentrafficsim.draw.DrawLevel;
29  import org.opentrafficsim.draw.OtsRenderable;
30  import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
31  import org.opentrafficsim.road.gtu.lane.perception.mental.Mental;
32  import org.opentrafficsim.road.gtu.lane.perception.mental.channel.ChannelFuller;
33  import org.opentrafficsim.road.gtu.lane.perception.mental.channel.ChannelTask;
34  import org.opentrafficsim.road.network.lane.conflict.Conflict;
35  
36  /**
37   * Draws circles around a GTU indicating the level of attention and perception delay.
38   * <p>
39   * Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
40   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
41   * </p>
42   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
43   */
44  public class PerceptionAnimation extends OtsRenderable<ChannelAttention>
45  {
46  
47      /** Maximum radius of attention circles. */
48      private static final double MAX_RADIUS = 1.425; // 1.5 minus half of LINE_WIDTH
49  
50      /** Radius around GTU along which the regular attention circles are placed. */
51      private static final double CENTER_RADIUS = 3.0;
52  
53      /** Radius around GTU along which the attention circles of objects are placed. */
54      private static final double CENTER_RADIUS_OBJECTS = 6.0;
55  
56      /** Line width around circle. */
57      private static final float LINE_WIDTH = 0.15f;
58  
59      /** Color scale for perception delay. */
60      private static final BoundsPaintScale SCALE =
61              new BoundsPaintScale(new double[] {0.0, 0.25, 0.5, 0.75, 1.0}, Colors.GREEN_RED_DARK);
62  
63      /**
64       * Constructor.
65       * @param gtu GTU
66       */
67      public PerceptionAnimation(final LaneBasedGtu gtu)
68      {
69          super(new ChannelAttention(gtu), gtu.getSimulator());
70      }
71  
72      @Override
73      public void paint(final Graphics2D graphics, final ImageObserver observer)
74      {
75          LaneBasedGtu gtu = getSource().getGtu();
76          Optional<Mental> mental = gtu.getTacticalPlanner().getPerception().getMental();
77          if (mental.isPresent() && mental.get() instanceof ChannelFuller fuller)
78          {
79              AffineTransform transform = graphics.getTransform();
80              graphics.setStroke(new BasicStroke(LINE_WIDTH));
81              graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
82  
83              Set<Object> channels = fuller.getChannels();
84              boolean hasInVehicle = channels.contains(ChannelTask.IN_VEHICLE);
85              for (Object channel : channels)
86              {
87                  double attention = fuller.getAttention(channel);
88                  Duration perceptionDelay = fuller.getPerceptionDelay(channel);
89                  double angle;
90                  double radius = CENTER_RADIUS;
91                  boolean drawLine = !hasInVehicle;
92                  if (ChannelTask.LEFT.equals(channel))
93                  {
94                      angle = Math.PI / 2.0;
95                  }
96                  else if (ChannelTask.FRONT.equals(channel))
97                  {
98                      angle = 0.0;
99                  }
100                 else if (ChannelTask.RIGHT.equals(channel))
101                 {
102                     angle = -Math.PI / 2.0;
103                 }
104                 else if (ChannelTask.REAR.equals(channel))
105                 {
106                     angle = Math.PI;
107                 }
108                 else if (ChannelTask.IN_VEHICLE.equals(channel))
109                 {
110                     angle = 0.0;
111                     radius = 0.0;
112                 }
113                 else if (channel instanceof OtsShape object)
114                 {
115                     Point2d point;
116                     if (channel instanceof Conflict conflict)
117                     {
118                         // on a conflict we take a point 25m upstream, or the upstream conflicting node if closer
119                         double x = conflict.getOtherConflict().getLongitudinalPosition().si - 25.0;
120                         point = conflict.getOtherConflict().getLane().getCenterLine().getLocationExtendedSI(x < 0.0 ? 0.0 : x);
121                     }
122                     else
123                     {
124                         point = object.getLocation();
125                     }
126                     angle = AngleUtil.normalizeAroundZero(gtu.getLocation().directionTo(point) - gtu.getLocation().dirZ);
127                     radius = CENTER_RADIUS_OBJECTS;
128                     drawLine = true;
129                 }
130                 else
131                 {
132                     continue;
133                 }
134                 drawAttentionCircle(graphics, gtu.getCenter().dx().si, attention, perceptionDelay, angle, radius, drawLine);
135                 graphics.setTransform(transform);
136             }
137         }
138 
139     }
140 
141     /**
142      * Draws attention circle.
143      * @param graphics graphics
144      * @param dx longitudinal shift
145      * @param attention attention level
146      * @param perceptionDelay perception delay
147      * @param angle angle to draw circle at relative to GTU
148      * @param radius center circle radius around GTU
149      * @param drawLine whether to draw the line
150      */
151     private static void drawAttentionCircle(final Graphics2D graphics, final double dx, final double attention,
152             final Duration perceptionDelay, final double angle, final double radius, final boolean drawLine)
153     {
154         // on center of GTU
155         graphics.translate(dx, 0.0);
156         graphics.rotate(-angle, 0.0, 0.0);
157 
158         // connecting line
159         if (drawLine)
160         {
161             graphics.setColor(Color.GRAY);
162             graphics.draw(new Line2D.Double(0.0, 0.0, radius - MAX_RADIUS - LINE_WIDTH, 0.0));
163         }
164 
165         // transparent background fill
166         Color color = SCALE.getPaint(Math.min(1.0, perceptionDelay.si));
167         graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 48));
168         graphics.fill(new Ellipse2D.Double(radius - MAX_RADIUS, -MAX_RADIUS, 2.0 * MAX_RADIUS, 2.0 * MAX_RADIUS));
169 
170         // non-transparent attention fill
171         graphics.setColor(color);
172         double r = Math.sqrt(attention);
173         graphics.fill(
174                 new Ellipse2D.Double(radius - r * MAX_RADIUS, -r * MAX_RADIUS, 2.0 * r * MAX_RADIUS, 2.0 * r * MAX_RADIUS));
175 
176         // edge of circle
177         graphics.setColor(Color.GRAY);
178         float lineWidth = LINE_WIDTH - 0.02f; // prevent tiny edges between fill and border
179         graphics.draw(new Ellipse2D.Double(radius - MAX_RADIUS - .5 * lineWidth, -MAX_RADIUS - .5 * lineWidth,
180                 2.0 * MAX_RADIUS + lineWidth, 2.0 * MAX_RADIUS + lineWidth));
181     }
182 
183     /**
184      * Paints the icon (Attention24.png).
185      * @param args not used
186      * @throws IOException if icon cannot be written
187      */
188     public static void main(final String[] args) throws IOException
189     {
190         BufferedImage im = new BufferedImage(24, 24, BufferedImage.TYPE_INT_ARGB);
191         Graphics2D g = (Graphics2D) im.getGraphics();
192         g.setStroke(new BasicStroke(LINE_WIDTH));
193         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
194         g.translate(12.0, 12.0);
195         g.scale(3.2, 3.2);
196         AffineTransform transform = g.getTransform();
197         drawAttentionCircle(g, 0.0, 0.8, Duration.ZERO, 0.25 * Math.PI, CENTER_RADIUS, true); // front
198         g.setTransform(transform);
199         drawAttentionCircle(g, 0.0, 0.4, Duration.ofSI(0.2), 0.75 * Math.PI, CENTER_RADIUS, true); // left
200         g.setTransform(transform);
201         drawAttentionCircle(g, 0.0, 0.2, Duration.ofSI(0.4), -0.25 * Math.PI, CENTER_RADIUS, true); // right
202         g.setTransform(transform);
203         drawAttentionCircle(g, 0.0, 0.1, Duration.ofSI(0.8), -0.75 * Math.PI, CENTER_RADIUS, true); // rear
204         File outputFile = new File(".." + File.separator + "ots-swing" + File.separator + "src" + File.separator + "main"
205                 + File.separator + "resources" + File.separator + "icons" + File.separator + "Perception24.png");
206         ImageIO.write(im, "png", outputFile);
207         System.out.println("Icon written to: " + outputFile.getAbsolutePath());
208     }
209 
210     /**
211      * Locatable for GTU in attention context.
212      */
213     public static class ChannelAttention implements OtsShape
214     {
215         /** GTU. */
216         private final LaneBasedGtu gtu;
217 
218         /**
219          * Constructor.
220          * @param gtu GTU
221          */
222         public ChannelAttention(final LaneBasedGtu gtu)
223         {
224             this.gtu = gtu;
225         }
226 
227         /**
228          * Returns the GTU.
229          * @return GTU
230          */
231         public LaneBasedGtu getGtu()
232         {
233             return this.gtu;
234         }
235 
236         @Override
237         public DirectedPoint2d getLocation()
238         {
239             return this.gtu.getLocation();
240         }
241 
242         @Override
243         public double getZ()
244         {
245             return DrawLevel.LABEL.getZ();
246         }
247 
248         @Override
249         public Polygon2d getRelativeContour()
250         {
251             return this.gtu.getRelativeContour();
252         }
253 
254     }
255 
256 }