PerceptionAnimation.java

package org.opentrafficsim.animation.gtu;

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.ImageObserver;
import java.util.Set;

import org.djunits.value.vdouble.scalar.Angle;
import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Length;
import org.djutils.exceptions.Throw;
import org.opentrafficsim.animation.BoundsPaintScale;
import org.opentrafficsim.animation.Colors;
import org.opentrafficsim.animation.OtsRenderable;
import org.opentrafficsim.animation.gtu.PerceptionAnimation.PerceptionData;
import org.opentrafficsim.animation.gtu.PerceptionAnimation.PerceptionData.ChannelData;
import org.opentrafficsim.animation.gtu.PerceptionAnimation.PerceptionData.ChannelRadius;
import org.opentrafficsim.base.geometry.OtsShape;

import nl.tudelft.simulation.naming.context.Contextualized;

/**
 * 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 Wouter Schakel
 */
public class PerceptionAnimation extends OtsRenderable<PerceptionData>
{

    /** 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 perceptionData perception data
     * @param contextualized contextualized, e.g. simulator
     */
    public PerceptionAnimation(final PerceptionData perceptionData, final Contextualized contextualized)
    {
        super(perceptionData, contextualized);
    }

    @Override
    public void paint(final Graphics2D graphics, final ImageObserver observer)
    {
        if (getSource().getChannels() == null || getSource().getChannels().isEmpty())
        {
            return;
        }

        AffineTransform transform = graphics.getTransform();
        graphics.setStroke(new BasicStroke(LINE_WIDTH));
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        boolean hasInVehicle = getSource().getChannels().stream().anyMatch((cd) -> cd.radius.equals(ChannelRadius.ZERO));
        double dx = getSource().getDeltaCenterX().si;
        for (ChannelData channelData : getSource().getChannels())
        {
            double radius = channelData.radius().equals(ChannelRadius.NEAR) ? CENTER_RADIUS
                    : (channelData.radius().equals(ChannelRadius.FAR) ? CENTER_RADIUS_OBJECTS : 0.0);
            boolean drawLine = (channelData.radius().equals(ChannelRadius.NEAR) && !hasInVehicle)
                    || channelData.radius().equals(ChannelRadius.FAR);
            drawAttentionCircle(graphics, dx, channelData.attention(), channelData.perceptionDelay().si, channelData.angle().si,
                    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 double 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));
        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));
    }

    /**
     * Perception animation data.
     */
    public interface PerceptionData extends OtsShape
    {

        /**
         * Returns the forward delta from the reference to the center of the channel animation. This value may be negative.
         * @return forward delta from the reference to the center of the channel animation
         */
        Length getDeltaCenterX();

        /**
         * Returns the channels.
         * @return channels
         */
        Set<ChannelData> getChannels();

        /**
         * Data required per channel.
         * @param angle angle relative to GTU
         * @param radius radius from GTU reference
         * @param attention attention level
         * @param perceptionDelay perception delay
         */
        record ChannelData(Angle angle, ChannelRadius radius, double attention, Duration perceptionDelay)
        {
            /**
             * Constructor.
             * @param angle angle relative to GTU
             * @param radius radius from GTU reference
             * @param attention attention level
             * @param perceptionDelay perception delay
             */
            public ChannelData
            {
                Throw.whenNull(angle, "angle");
                Throw.whenNull(radius, "radius");
                Throw.whenNull(attention, "attention");
                Throw.whenNull(perceptionDelay, "perceptionDelay");
            }
        }

        /**
         * Radius type.
         */
        enum ChannelRadius
        {
            /** Drawn with 0 radius. */
            ZERO,

            /** Drawn with small radius. */
            NEAR,

            /** Drawn with large radius. */
            FAR;
        }

    }

}