TrafficLightDetector.java

package org.opentrafficsim.road.network.lane.object.detector;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.djunits.value.vdouble.scalar.Length;
import org.djutils.draw.line.Polygon2d;
import org.djutils.draw.line.Ray2d;
import org.djutils.draw.point.OrientedPoint2d;
import org.djutils.draw.point.Point2d;
import org.djutils.event.Event;
import org.djutils.event.EventListener;
import org.djutils.event.EventType;
import org.djutils.event.LocalEventProducer;
import org.djutils.exceptions.Throw;
import org.djutils.metadata.MetaData;
import org.djutils.metadata.ObjectDescriptor;
import org.opentrafficsim.base.geometry.BoundingPolygon;
import org.opentrafficsim.base.geometry.OtsBounds2d;
import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
import org.opentrafficsim.core.geometry.OtsGeometryException;
import org.opentrafficsim.core.geometry.OtsLine2d;
import org.opentrafficsim.core.gtu.GtuException;
import org.opentrafficsim.core.gtu.RelativePosition;
import org.opentrafficsim.core.gtu.RelativePosition.Type;
import org.opentrafficsim.core.network.Network;
import org.opentrafficsim.core.network.NetworkException;
import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
import org.opentrafficsim.road.network.lane.Lane;

/**
 * This traffic light reports whether any GTUs are within its area. The area is two sub-sections on one or two lanes. This
 * traffic does <b>not</b> report the total number of GTUs within the area; only whether that number is zero or non-zero. This
 * class does not derive from {@code Detector} as it concerns an area, not a cross-section. All sides of the 2 areas are managed
 * by 4 {@code Detector}s to capture GTU longitudinal movement, and by listening to events to capture lane changes, vehicle
 * generation, and vehicle destruction.
 * <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://github.com/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 */
public class TrafficLightDetector extends LocalEventProducer implements EventListener, Detector
{
    /** */
    private static final long serialVersionUID = 20161103L;

    /** Id of this TrafficLightDetector. */
    private final String id;

    /** Unique id for network. */
    private final String uniqueId;

    /** The detector that detects when a GTU enters the detector area at point A. */
    private final StartEndDetector entryA;

    /** The detector that detects when a GTU exits the detector area at point B. */
    private final StartEndDetector exitB;

    /** GTUs detected by the entrance detectors, but not yet removed by the exit detectors. */
    private final Set<LaneBasedGtu> currentGTUs = new LinkedHashSet<>();

    /** The lanes that the detector (partly) covers. */
    private final Set<Lane> lanes = new LinkedHashSet<>();

    /** The OTS network. */
    private final Network network;

    /** Type. */
    private final DetectorType type;

    /** Center location. */
    private final OrientedPoint2d location;

    /** Geometry of the detector. */
    private final BoundingPolygon geometry;

    /**
     * The <b>timed</b> event type for pub/sub indicating the triggering of the entry of a NonDirectionalOccupancyDetector. <br>
     * Payload: Object[] {String detectorId}
     */
    public static final EventType TRAFFIC_LIGHT_DETECTOR_TRIGGER_ENTRY_EVENT =
            new EventType("TRAFFICLIGHTDETECTOR.TRIGGER.ENTRY",
                    new MetaData("Traffic light detector entty", "Traffic light detector was entered",
                            new ObjectDescriptor("Detector id", "Traffic light detector id", String.class)));

    /**
     * The <b>timed</b> event type for pub/sub indicating the triggering of the exit of an NonDirectionalOccupancyDetector. <br>
     * Payload: Object[] {String detectorId}
     */
    public static final EventType TRAFFIC_LIGHT_DETECTOR_TRIGGER_EXIT_EVENT = new EventType("TRAFFICLIGHTDETECTOR.TRIGGER.EXIT",
            new MetaData("Traffic light detector exit", "Traffic light detector was exited",
                    new ObjectDescriptor("Detector id", "Traffic light detector id", String.class)));

    /**
     * Construct a new traffic light detector.<br>
     * TODO Possibly provide the GtuTypes that trigger the detector as an argument for the constructor
     * @param id String; id of this detector
     * @param laneA Lane; the lane of the A detection point of this traffic light detector
     * @param positionA Length; the position of the A detection point of this traffic light detector
     * @param laneB Lane; the lane of the B detection point of this traffic light detector
     * @param positionB Length; the position of the B detection point of this traffic light detector
     * @param intermediateLanes List&lt;Lane&gt;; list of intermediate lanes
     * @param entryPosition TYPE; the position on the GTUs that trigger the entry events
     * @param exitPosition TYPE; the position on the GTUs that trigger the exit events
     * @param simulator OtsSimulatorInterface; the simulator
     * @param detectorType DetectorType; detector type.
     * @throws NetworkException when the network is inconsistent.
     */
    @SuppressWarnings("checkstyle:parameternumber")
    public TrafficLightDetector(final String id, final Lane laneA, final Length positionA, final Lane laneB,
            final Length positionB, final List<Lane> intermediateLanes, final Type entryPosition, final Type exitPosition,
            final OtsSimulatorInterface simulator, final DetectorType detectorType) throws NetworkException
    {
        Throw.whenNull(id, "id may not be null");
        this.id = id;
        this.uniqueId = UUID.randomUUID().toString() + "_" + id;
        this.type = detectorType;
        this.entryA = new StartEndDetector(id + ".entryA", laneA, positionA, entryPosition, simulator, detectorType);
        this.exitB = new StartEndDetector(id + ".exitB", laneB, positionB, exitPosition, simulator, detectorType);
        // Set up detection of GTUs that enter or leave the detector laterally or appear due to a generator or disappear due to
        // a sink
        this.lanes.add(laneA);
        this.network = laneA.getLink().getNetwork();
        if (null != intermediateLanes)
        {
            this.lanes.addAll(intermediateLanes);
        }
        this.lanes.add(laneB);
        for (Lane lane : this.lanes)
        {
            lane.addListener(this, Lane.GTU_ADD_EVENT);
            lane.addListener(this, Lane.GTU_REMOVE_EVENT);
        }
        try
        {
            OtsLine2d path;
            if (this.lanes.size() == 1)
            {
                path = laneA.getCenterLine().extract(positionA, positionB);
            }
            else
            {
                List<Point2d> pathPoints = new ArrayList<>();
                pathPoints.addAll(Arrays.asList(laneA.getCenterLine().extract(positionA, laneA.getLength()).getPoints()));
                for (Lane intermediateLane : intermediateLanes)
                {
                    pathPoints.addAll(Arrays.asList(intermediateLane.getCenterLine().getPoints()));
                }
                pathPoints.addAll(Arrays.asList(laneB.getCenterLine().extract(Length.ZERO, positionB).getPoints()));
                path = OtsLine2d.createAndCleanOtsLine2d(pathPoints);
            }
            OtsLine2d left = path.offsetLine(0.5);
            OtsLine2d right = path.offsetLine(-0.5);
            Ray2d ray = path.getLine2d().getLocationFraction(0.5);
            double dx = ray.x;
            double dy = ray.y;
            this.location = new OrientedPoint2d(dx, dy);
            List<Point2d> geometryPoints = new ArrayList<>();
            geometryPoints.add(new Point2d(right.get(0).x - dx, right.get(0).y - dy));
            for (Point2d p : left.getPoints())
            {
                geometryPoints.add(new Point2d(p.x - dx, p.y - dy));
            }
            for (Point2d p : right.reverse().getPoints())
            {
                geometryPoints.add(new Point2d(p.x - dx, p.y - dy));
            }
            this.geometry = new BoundingPolygon(new Polygon2d(geometryPoints));
        }
        catch (OtsGeometryException exception)
        {
            throw new NetworkException("Points A and B may be the same.", exception);
        }
        this.network.addObject(this);
    }

    /**
     * Add a GTU to the set.
     * @param gtu LaneBasedGtu; the GTU that must be added
     */
    protected final void addGtu(final LaneBasedGtu gtu)
    {
        if (this.currentGTUs.add(gtu) && this.currentGTUs.size() == 1)
        {
            fireTimedEvent(TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_ENTRY_EVENT, new Object[] {getId()},
                    getSimulator().getSimulatorTime());
        }
    }

    /**
     * Remove a GTU from the set.
     * @param gtu LaneBasedGtu; the GTU that must be removed
     */
    protected final void removeGtu(final LaneBasedGtu gtu)
    {
        if (this.currentGTUs.remove(gtu) && this.currentGTUs.size() == 0)
        {
            fireTimedEvent(TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_EXIT_EVENT, new Object[] {getId()},
                    getSimulator().getSimulatorTime());
        }
    }

    /** {@inheritDoc} */
    @Override
    public final void notify(final Event event) throws RemoteException
    {
        String gtuId = (String) ((Object[]) event.getContent())[0];
        LaneBasedGtu gtu = (LaneBasedGtu) this.network.getGTU(gtuId);
        if (Lane.GTU_REMOVE_EVENT.equals(event.getType()))
        {
            if (!this.currentGTUs.contains(gtu))
            {
                return; // GTU is not currently detected; nothing to do
            }
            try
            {
                // If the detector covers only (part of) one lane, this must have triggered this event, GTU on longer on det.
                if (this.lanes.size() == 1)
                {
                    removeGtu(gtu);
                    return;
                }

                Lane lane = null;
                String laneId = (String) ((Object[]) event.getContent())[4];
                String linkId = (String) ((Object[]) event.getContent())[5];
                for (Lane detectorLane : this.lanes)
                {
                    if (detectorLane.getId().equals(laneId) && detectorLane.getLink().getId().equals(linkId))
                    {
                        lane = detectorLane;
                        break;
                    }
                }

                Set<Lane> remainingLanes = gtu.positions(gtu.getRelativePositions().get(RelativePosition.CENTER)).keySet();
                remainingLanes.retainAll(this.lanes);
                remainingLanes.remove(lane); // still in positions during this event
                if (remainingLanes.isEmpty())
                {
                    removeGtu(gtu);
                }
                // else: GTU is still in one of our lanes and we will get another GTU_REMOVE_EVENT or the GTU will trigger one
                // of our exit flank detectors or when the GTU leaves this detector laterally
                return;
            }
            catch (GtuException exception)
            {
                System.err.println("Caught GTU exception trying to get the a position");
                exception.printStackTrace();
            }
        }
        else if (Lane.GTU_ADD_EVENT.equals(event.getType()))
        {
            if (this.currentGTUs.contains(gtu))
            {
                return; // GTU is already detected; nothing to do
            }
            // Determine whether the GTU is in our range
            try
            {
                // If the detector covers only (part of) one lane, this must have triggered this event, check position on it
                if (this.lanes.size() == 1)
                {
                    Lane lane = this.lanes.iterator().next();
                    Length frontPos = gtu.position(lane, gtu.getRelativePositions().get(this.entryA.getPositionType()));
                    Length rearPos = gtu.position(lane, gtu.getRelativePositions().get(this.exitB.getPositionType()));
                    if (frontPos.gt(this.entryA.getLongitudinalPosition()) && rearPos.lt(this.exitB.getLongitudinalPosition()))
                    {
                        addGtu(gtu);
                    }
                    return;
                }

                Lane lane = null;
                String laneId = (String) ((Object[]) event.getContent())[2];
                String linkId = (String) ((Object[]) event.getContent())[3];
                for (Lane detectorLane : this.lanes)
                {
                    if (detectorLane.getId().equals(laneId) && detectorLane.getLink().getId().equals(linkId))
                    {
                        lane = detectorLane;
                    }
                }

                // If the triggering lane neither contains A nor B, it is an intermediate lane, so the GTU is on the detector
                if (!this.entryA.getLane().equals(lane) && !this.exitB.getLane().equals(lane))
                {
                    addGtu(gtu);
                    return;
                }

                // If triggering lane contains A, detector is triggered if front is beyond A (remainder of lane is all detector)
                if (this.entryA.getLane().equals(lane))
                {
                    Length frontPos =
                            gtu.position(this.entryA.getLane(), gtu.getRelativePositions().get(this.entryA.getPositionType()));
                    if (frontPos.gt(this.entryA.getLongitudinalPosition()))
                    {
                        addGtu(gtu);
                    }
                    return;
                }

                // If triggering lane contains B, detector is triggered if the rear is before B (before on lane is all detector)
                if (this.exitB.getLane().equals(lane))
                {
                    Length rearPos =
                            gtu.position(this.exitB.getLane(), gtu.getRelativePositions().get(this.exitB.getPositionType()));
                    if (rearPos.lt(this.exitB.getLongitudinalPosition()))
                    {
                        addGtu(gtu);
                    }
                    return;
                }

                throw new RuntimeException("Traffic light detector was notified that "
                        + "a GTU was added to a lane, but could not figure out what to do with it.");
            }
            catch (GtuException exception)
            {
                System.err.println("Caught GTU exception trying to get a position");
                exception.printStackTrace();
            }
        }
        else
        {
            System.err.println("Unexpected event: " + event);
        }
    }

    /** @return the relative position type of the vehicle (e.g., FRONT, BACK) that triggers the detector. */
    public final Type getPositionTypeEntry()
    {
        return this.entryA.getPositionType();
    }

    /** @return the relative position type of the vehicle (e.g., FRONT, BACK) that triggers the detector. */
    public final Type getPositionTypeExit()
    {
        return this.exitB.getPositionType();
    }

    /**
     * Return the A position of this NonDirectionalOccupancyDetector.
     * @return Length; the lane and position on the lane where GTU entry is detected
     */
    public final Length getLanePositionA()
    {
        return this.entryA.getLongitudinalPosition();
    }

    /**
     * Return the B position of this NonDirectionalOccupancyDetector.
     * @return Length; the lane and position on the lane where GTU exit is detected
     */
    public final Length getLanePositionB()
    {
        return this.exitB.getLongitudinalPosition();
    }

    /**
     * One of our start/end detectors has triggered.
     * @param detector StartEndDetector; the detector that was triggered
     * @param gtu LaneBasedGtu; the gtu that triggered the flank detector
     */
    public final void signalDetection(final StartEndDetector detector, final LaneBasedGtu gtu)
    {
        if (this.entryA.equals(detector))// || this.entryB == detector)
        {
            addGtu(gtu);
        }
        else if (this.exitB.equals(detector))// || this.exitA == detector)
        {
            removeGtu(gtu);
        }
    }

    /**
     * Returns the id.
     * @return The id of the detector.
     */
    @Override
    public final String getId()
    {
        return this.id;
    }

    /**
     * Returns the simulator.
     * @return The simulator.
     */
    public final OtsSimulatorInterface getSimulator()
    {
        return this.entryA.getSimulator();
    }

    /** {@inheritDoc} */
    @Override
    public final OrientedPoint2d getLocation()
    {
        return this.location;
    }

    /** {@inheritDoc} */
    @Override
    public final OtsBounds2d getBounds()
    {
        return this.geometry;
    }

    /**
     * Return the state of this traffic light detector.
     * @return boolean; true if one or more GTUs are currently detected; false of no GTUs are currently detected
     */
    public final boolean getOccupancy()
    {
        return this.currentGTUs.size() > 0;
    }

    /** {@inheritDoc} */
    @Override
    public Polygon2d getGeometry()
    {
        return this.geometry.asPolygon();
    }

    /** {@inheritDoc} */
    @Override
    public Length getHeight()
    {
        return Length.ZERO;
    }

    /** {@inheritDoc} */
    @Override
    public String getFullId()
    {
        return this.uniqueId;
    }

    /** {@inheritDoc} */
    @Override
    public DetectorType getType()
    {
        return this.type;
    }

    /** {@inheritDoc} */
    @Override
    public final String toString()
    {
        return "TrafficLightDetector [id=" + this.id + ", entryA=" + this.entryA + ", exitB=" + this.exitB + ", currentGTUs="
                + this.currentGTUs + ", lanes=" + this.lanes + ", geometry=" + this.geometry + "]";
    }

    /**
     * Embedded detectors used by a TrafficLightDetector.
     * <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://github.com/averbraeck">Alexander Verbraeck</a>
     * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
     * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
     */
    public class StartEndDetector extends LaneDetector
    {
        /** */
        private static final long serialVersionUID = 20161104L;

        /**
         * Construct a new StartEndDetector.
         * @param id String; the name of the new StartEndDetector
         * @param lane Lane; the lane of the new StartEndDetector
         * @param longitudinalPosition Length; the longitudinal position of the new StartEndDetector
         * @param positionType TYPE; the position on the GTUs that triggers the new StartEndDetector
         * @param simulator OtsSimulatorInterface; the simulator engine
         * @param detectorType DetectorType; detector type.
         * @throws NetworkException when the network is inconsistent
         */
        public StartEndDetector(final String id, final Lane lane, final Length longitudinalPosition, final Type positionType,
                final OtsSimulatorInterface simulator, final DetectorType detectorType) throws NetworkException
        {
            super(id, lane, longitudinalPosition, positionType, simulator, detectorType);
        }

        /** {@inheritDoc} */
        @Override
        protected final void triggerResponse(final LaneBasedGtu gtu)
        {
            TrafficLightDetector.this.signalDetection(this, gtu);
        }

        /** {@inheritDoc} */
        @Override
        public final String toString()
        {
            return "StartEndDetector [parent=" + TrafficLightDetector.this.getId() + "]";
        }

        /**
         * Returns the parent TrafficLightDetector.
         * @return TrafficLightDetector; parent.
         */
        public TrafficLightDetector getParent()
        {
            return TrafficLightDetector.this;
        }

    }

}