PerceptionAnimation.java
package org.opentrafficsim.animation;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import javax.imageio.ImageIO;
import org.djunits.value.vdouble.scalar.Duration;
import org.djutils.draw.line.Polygon2d;
import org.djutils.draw.point.DirectedPoint2d;
import org.djutils.draw.point.Point2d;
import org.djutils.math.AngleUtil;
import org.opentrafficsim.animation.PerceptionAnimation.ChannelAttention;
import org.opentrafficsim.base.geometry.OtsShape;
import org.opentrafficsim.draw.BoundsPaintScale;
import org.opentrafficsim.draw.Colors;
import org.opentrafficsim.draw.DrawLevel;
import org.opentrafficsim.draw.OtsRenderable;
import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
import org.opentrafficsim.road.gtu.lane.perception.mental.Mental;
import org.opentrafficsim.road.gtu.lane.perception.mental.channel.ChannelFuller;
import org.opentrafficsim.road.gtu.lane.perception.mental.channel.ChannelTask;
import org.opentrafficsim.road.network.lane.conflict.Conflict;
/**
* Draws circles around a GTU indicating the level of attention and perception delay.
* <p>
* Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public class PerceptionAnimation extends OtsRenderable<ChannelAttention>
{
/** Maximum radius of attention circles. */
private static final double MAX_RADIUS = 1.425; // 1.5 minus half of LINE_WIDTH
/** Radius around GTU along which the regular attention circles are placed. */
private static final double CENTER_RADIUS = 3.0;
/** Radius around GTU along which the attention circles of objects are placed. */
private static final double CENTER_RADIUS_OBJECTS = 6.0;
/** Line width around circle. */
private static final float LINE_WIDTH = 0.15f;
/** Color scale for perception delay. */
private static final BoundsPaintScale SCALE =
new BoundsPaintScale(new double[] {0.0, 0.25, 0.5, 0.75, 1.0}, Colors.GREEN_RED_DARK);
/**
* Constructor.
* @param gtu GTU
*/
public PerceptionAnimation(final LaneBasedGtu gtu)
{
super(new ChannelAttention(gtu), gtu.getSimulator());
}
@Override
public void paint(final Graphics2D graphics, final ImageObserver observer)
{
LaneBasedGtu gtu = getSource().getGtu();
Optional<Mental> mental = gtu.getTacticalPlanner().getPerception().getMental();
if (mental.isPresent() && mental.get() instanceof ChannelFuller fuller)
{
AffineTransform transform = graphics.getTransform();
graphics.setStroke(new BasicStroke(LINE_WIDTH));
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Set<Object> channels = fuller.getChannels();
boolean hasInVehicle = channels.contains(ChannelTask.IN_VEHICLE);
for (Object channel : channels)
{
double attention = fuller.getAttention(channel);
Duration perceptionDelay = fuller.getPerceptionDelay(channel);
double angle;
double radius = CENTER_RADIUS;
boolean drawLine = !hasInVehicle;
if (ChannelTask.LEFT.equals(channel))
{
angle = Math.PI / 2.0;
}
else if (ChannelTask.FRONT.equals(channel))
{
angle = 0.0;
}
else if (ChannelTask.RIGHT.equals(channel))
{
angle = -Math.PI / 2.0;
}
else if (ChannelTask.REAR.equals(channel))
{
angle = Math.PI;
}
else if (ChannelTask.IN_VEHICLE.equals(channel))
{
angle = 0.0;
radius = 0.0;
}
else if (channel instanceof OtsShape object)
{
Point2d point;
if (channel instanceof Conflict conflict)
{
// on a conflict we take a point 25m upstream, or the upstream conflicting node if closer
double x = conflict.getOtherConflict().getLongitudinalPosition().si - 25.0;
point = conflict.getOtherConflict().getLane().getCenterLine().getLocationExtendedSI(x < 0.0 ? 0.0 : x);
}
else
{
point = object.getLocation();
}
angle = AngleUtil.normalizeAroundZero(gtu.getLocation().directionTo(point) - gtu.getLocation().dirZ);
radius = CENTER_RADIUS_OBJECTS;
drawLine = true;
}
else
{
continue;
}
drawAttentionCircle(graphics, gtu.getCenter().dx().si, attention, perceptionDelay, angle, radius, drawLine);
graphics.setTransform(transform);
}
}
}
/**
* Draws attention circle.
* @param graphics graphics
* @param dx longitudinal shift
* @param attention attention level
* @param perceptionDelay perception delay
* @param angle angle to draw circle at relative to GTU
* @param radius center circle radius around GTU
* @param drawLine whether to draw the line
*/
private static void drawAttentionCircle(final Graphics2D graphics, final double dx, final double attention,
final Duration perceptionDelay, final double angle, final double radius, final boolean drawLine)
{
// on center of GTU
graphics.translate(dx, 0.0);
graphics.rotate(-angle, 0.0, 0.0);
// connecting line
if (drawLine)
{
graphics.setColor(Color.GRAY);
graphics.draw(new Line2D.Double(0.0, 0.0, radius - MAX_RADIUS - LINE_WIDTH, 0.0));
}
// transparent background fill
Color color = SCALE.getPaint(Math.min(1.0, perceptionDelay.si));
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 48));
graphics.fill(new Ellipse2D.Double(radius - MAX_RADIUS, -MAX_RADIUS, 2.0 * MAX_RADIUS, 2.0 * MAX_RADIUS));
// non-transparent attention fill
graphics.setColor(color);
double r = Math.sqrt(attention);
graphics.fill(
new Ellipse2D.Double(radius - r * MAX_RADIUS, -r * MAX_RADIUS, 2.0 * r * MAX_RADIUS, 2.0 * r * MAX_RADIUS));
// edge of circle
graphics.setColor(Color.GRAY);
float lineWidth = LINE_WIDTH - 0.02f; // prevent tiny edges between fill and border
graphics.draw(new Ellipse2D.Double(radius - MAX_RADIUS - .5 * lineWidth, -MAX_RADIUS - .5 * lineWidth,
2.0 * MAX_RADIUS + lineWidth, 2.0 * MAX_RADIUS + lineWidth));
}
/**
* Paints the icon (Attention24.png).
* @param args not used
* @throws IOException if icon cannot be written
*/
public static void main(final String[] args) throws IOException
{
BufferedImage im = new BufferedImage(24, 24, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D) im.getGraphics();
g.setStroke(new BasicStroke(LINE_WIDTH));
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.translate(12.0, 12.0);
g.scale(3.2, 3.2);
AffineTransform transform = g.getTransform();
drawAttentionCircle(g, 0.0, 0.8, Duration.ZERO, 0.25 * Math.PI, CENTER_RADIUS, true); // front
g.setTransform(transform);
drawAttentionCircle(g, 0.0, 0.4, Duration.ofSI(0.2), 0.75 * Math.PI, CENTER_RADIUS, true); // left
g.setTransform(transform);
drawAttentionCircle(g, 0.0, 0.2, Duration.ofSI(0.4), -0.25 * Math.PI, CENTER_RADIUS, true); // right
g.setTransform(transform);
drawAttentionCircle(g, 0.0, 0.1, Duration.ofSI(0.8), -0.75 * Math.PI, CENTER_RADIUS, true); // rear
File outputFile = new File(".." + File.separator + "ots-swing" + File.separator + "src" + File.separator + "main"
+ File.separator + "resources" + File.separator + "icons" + File.separator + "Perception24.png");
ImageIO.write(im, "png", outputFile);
System.out.println("Icon written to: " + outputFile.getAbsolutePath());
}
/**
* Locatable for GTU in attention context.
*/
public static class ChannelAttention implements OtsShape
{
/** GTU. */
private final LaneBasedGtu gtu;
/**
* Constructor.
* @param gtu GTU
*/
public ChannelAttention(final LaneBasedGtu gtu)
{
this.gtu = gtu;
}
/**
* Returns the GTU.
* @return GTU
*/
public LaneBasedGtu getGtu()
{
return this.gtu;
}
@Override
public DirectedPoint2d getLocation()
{
return this.gtu.getLocation();
}
@Override
public double getZ()
{
return DrawLevel.LABEL.getZ();
}
@Override
public Polygon2d getRelativeContour()
{
return this.gtu.getRelativeContour();
}
}
}