TrafCodDisplay.java

package org.opentrafficsim.trafficcontrol.trafcod;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.rmi.RemoteException;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.swing.JPanel;
import javax.swing.ToolTipManager;

import org.djutils.event.Event;
import org.djutils.event.EventListener;
import org.djutils.event.LocalEventProducer;
import org.opentrafficsim.road.network.lane.object.detector.TrafficLightDetector;
import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLightColor;

/**
 * Display the current state of a TrafCOD machine.
 * <p>
 * Copyright (c) 2013-2024 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://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
 */
public class TrafCodDisplay extends JPanel implements MouseMotionListener, MouseListener
{
    /** */
    private static final long serialVersionUID = 20161115L;

    /** Background image. */
    private final BufferedImage image;

    /** The set of objects drawn on the image. */
    private Set<TrafCODObject> trafCODObjects = new LinkedHashSet<>();

    /** Store the tool tip delay so we can restore it when the mouse exits this TrafCODDisplay. */
    private final int defaultInitialDelay = ToolTipManager.sharedInstance().getInitialDelay();

    /**
     * Construct a new TrafCODDisplay.
     * @param image BufferedImage; the background image. This constructor does <b>not</b> make a deep copy of the image.
     *            Modifications of the image after calling this constructor might have <i>interesting</i> consequences, but
     *            should not result in crashes.
     */
    public TrafCodDisplay(final BufferedImage image)
    {
        this.image = image;
        super.setPreferredSize(new Dimension(this.image.getWidth(), this.image.getHeight()));
        addMouseMotionListener(this);
    }

    /**
     * Look up a DetectorImage.
     * @param id String; id of the DetectorImage
     * @return DetectorImage; the detector image with matching id or null.
     */
    public DetectorImage getDetectorImage(final String id)
    {
        for (TrafCODObject tco : this.trafCODObjects)
        {
            if (tco instanceof DetectorImage && ((DetectorImage) tco).getId().equals(id))
            {
                return (DetectorImage) tco;
            }
        }
        return null;
    }

    /** {@inheritDoc} */
    @Override
    protected void paintComponent(final Graphics g)
    {
        super.paintComponent(g);
        g.drawImage(this.image, 0, 0, null);
        for (TrafCODObject tco : this.trafCODObjects)
        {
            tco.draw((Graphics2D) g);
        }
    }

    /**
     * Add one TrafCODObject to this TrafCODDisplay.
     * @param trafCODObject TrafCODObject; the TrafCOD object that must be added
     */
    void addTrafCODObject(final TrafCODObject trafCODObject)
    {
        this.trafCODObjects.add(trafCODObject);
    }

    /** {@inheritDoc} */
    @Override
    public void mouseDragged(final MouseEvent e)
    {
        mouseMoved(e); // Do the same as in the mouse move event
    }

    /** {@inheritDoc} */
    @Override
    public void mouseMoved(final MouseEvent e)
    {
        String toolTipText = null;
        for (TrafCODObject tco : this.trafCODObjects)
        {
            toolTipText = tco.toolTipHit(e.getX(), e.getY());
            if (null != toolTipText)
            {
                break;
            }
        }
        // System.out.println("Setting tool tip text to " + toolTipText);
        setToolTipText(toolTipText);
    }

    /** {@inheritDoc} */
    @Override
    public void mouseClicked(final MouseEvent e)
    {
        // Ignore
    }

    /** {@inheritDoc} */
    @Override
    public void mousePressed(final MouseEvent e)
    {
        // Ignore
    }

    /** {@inheritDoc} */
    @Override
    public void mouseReleased(final MouseEvent e)
    {
        // Ignore
    }

    /** {@inheritDoc} */
    @Override
    public void mouseEntered(final MouseEvent e)
    {
        ToolTipManager.sharedInstance().setInitialDelay(0);
    }

    /** {@inheritDoc} */
    @Override
    public void mouseExited(final MouseEvent e)
    {
        ToolTipManager.sharedInstance().setInitialDelay(this.defaultInitialDelay);
    }

}

/**
 * Interface for objects that can draw themselves onto a Graphics2D and may want to show their own tool tip text when the mouse
 * hits them.
 */
interface TrafCODObject
{
    /**
     * Draw yourself at the indicated location.
     * @param g2 Graphics2D; the graphics context
     */
    void draw(Graphics2D g2);

    /**
     * Check if the given coordinates hit the TrafCODObject. If it does return a String to be used as a tool tip text. If the
     * coordinates do not hit this TrafCODObject return null.
     * @param testX int; the x-coordinate
     * @param testY int; the y-coordinate
     * @return String; the tool tip text or null if the coordinates do not hit the TrafCodObject
     */
    String toolTipHit(int testX, int testY);

}

/**
 * Draws a detector.
 */
class DetectorImage implements TrafCODObject, EventListener
{
    /** ... */
    private static final long serialVersionUID = 20200313L;

    /** The TrafCOD display. */
    private final TrafCodDisplay display;

    /** X-coordinate on the TrafCOD display image where this traffic light must be drawn. */
    private final int x;

    /** Y-coordinate on the TrafCOD display image where this traffic light must be drawn. */
    private final int y;

    /** Tool tip text for this detector image. */
    private final String description;

    /** String used to match this detector with the TrafCOD detector input. */
    private final String id;

    /** Fill color (used to indicate the occupancy state of the detector). */
    private Color fillColor = Color.WHITE;

    /** Size of the box that is drawn. */
    private static final int BOX_SIZE = 13;

    /** Correction to make the result match that of the C++Builder version. */
    private static final int X_OFFSET = 5;

    /** Correction to make the result match that of the C++Builder version. */
    private static final int Y_OFFSET = 5;

    /**
     * Construct a new DetectorImage.
     * @param display TrafCODDisplay; the TrafCOD display on which this detector image will be rendered
     * @param center Point2D; the center location of the detector image on the TrafCOD display
     * @param id String; id used to match this detector with the TrafCOD detector input
     * @param description String; name of the detector (displayed as tool tip text)
     */
    DetectorImage(final TrafCodDisplay display, final Point2D center, final String id, final String description)
    {
        this.display = display;
        this.x = (int) center.getX();
        this.y = (int) center.getY();
        this.id = id;
        this.description = description;
        display.addTrafCODObject(this);
    }

    /** {@inheritDoc} */
    @Override
    public void draw(final Graphics2D g2)
    {
        g2.setColor(this.fillColor);
        g2.fillRect(X_OFFSET + this.x - BOX_SIZE / 2, Y_OFFSET + this.y - BOX_SIZE / 2, BOX_SIZE, BOX_SIZE);
        g2.setColor(Color.BLACK);
        g2.drawRect(X_OFFSET + this.x - BOX_SIZE / 2, Y_OFFSET + this.y - BOX_SIZE / 2, BOX_SIZE, BOX_SIZE);
    }

    /** {@inheritDoc} */
    @Override
    public void notify(final Event event) throws RemoteException
    {
        if (event.getType().equals(TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_ENTRY_EVENT))
        {
            this.fillColor = Color.BLUE;
        }
        else if (event.getType().equals(TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_EXIT_EVENT))
        {
            this.fillColor = Color.WHITE;
        }
        this.display.repaint();
    }

    /** {@inheritDoc} */
    @Override
    public String toolTipHit(final int testX, final int testY)
    {
        if (testX < X_OFFSET + this.x - BOX_SIZE / 2 || testX >= X_OFFSET + this.x + BOX_SIZE / 2
                || testY < Y_OFFSET - BOX_SIZE / 2 + this.y || testY >= Y_OFFSET + this.y + BOX_SIZE / 2)
        {
            return null;
        }
        return this.description;
    }

    /**
     * Retrieve the id of this DetectorImage.
     * @return String; the id of this DetectorImage
     */
    public String getId()
    {
        return this.id;
    }

}

/**
 * Draws a traffic light. <br>
 */
class TrafficLightImage extends LocalEventProducer implements TrafCODObject
{
    /** ... */
    private static final long serialVersionUID = 20200313L;

    /** The TrafCOD display. */
    private final TrafCodDisplay display;

    /** X-coordinate on the TrafCOD display image where this traffic light must be drawn. */
    private final int x;

    /** Y-coordinate on the TrafCOD display image where this traffic light must be drawn. */
    private final int y;

    /** Tool tip text for this traffic light image. */
    private final String description;

    /** The current color. */
    private TrafficLightColor color = TrafficLightColor.BLACK;

    /**
     * Create a traffic light image.
     * @param display TrafCODDisplay; the TrafCOD display on which this traffic light image will be rendered
     * @param center Point2D; coordinates in the image where this traffic light is centered on
     * @param description String; tool tip text for the new traffic light image
     */
    TrafficLightImage(final TrafCodDisplay display, final Point2D center, final String description)
    {
        this.display = display;
        this.x = (int) center.getX();
        this.y = (int) center.getY();
        this.description = description;
        display.addTrafCODObject(this);
    }

    /** {@inheritDoc} */
    @Override
    public String toolTipHit(final int testX, final int testY)
    {
        if (testX < this.x - DISC_SIZE / 2 || testX >= this.x + DISC_SIZE / 2 || testY < this.y - DISC_SIZE / 2
                || testY >= this.y + DISC_SIZE / 2)
        {
            return null;
        }
        return this.description;
    }

    /**
     * Sets the traffic light color.
     * @param trafficLightColor TrafficLightColor; traffic light color.
     */
    public void setTrafficLightColor(final TrafficLightColor trafficLightColor)
    {
        this.color = trafficLightColor;
        this.display.repaint();
    }

    /** Diameter of a traffic light disk in pixels. */
    private static final int DISC_SIZE = 11;

    /** {@inheritDoc} */
    @Override
    public void draw(final Graphics2D g2)
    {
        Color lightColor;
        switch (this.color)
        {
            case BLACK:
                lightColor = Color.BLACK;
                break;

            case GREEN:
                lightColor = Color.green;
                break;

            case YELLOW:
                lightColor = Color.YELLOW;
                break;

            case RED:
                lightColor = Color.RED;
                break;

            default:
                System.err.println("Unhandled TrafficLightColor: " + this.color);
                return;
        }
        g2.setColor(lightColor);
        g2.fillOval(this.x - DISC_SIZE / 2, this.y - DISC_SIZE / 2, DISC_SIZE, DISC_SIZE);
        // System.out.println("Drawn disk in color " + lightColor);
    }

}