DistractionField.java

package org.opentrafficsim.road.gtu.lane.perception.mental;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.function.BiFunction;

import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Length;
import org.djutils.event.Event;
import org.djutils.event.EventListener;
import org.opentrafficsim.base.parameters.ParameterException;
import org.opentrafficsim.core.gtu.RelativePosition;
import org.opentrafficsim.core.network.LateralDirectionality;
import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
import org.opentrafficsim.road.gtu.lane.perception.structure.LaneStructure;
import org.opentrafficsim.road.gtu.lane.perception.structure.NavigatingIterable.Entry;
import org.opentrafficsim.road.network.lane.object.RoadSideDistraction;

/**
 * This class perceives all distractions. It stores information on lane, odometer and task demand per distraction. For
 * distraction objects that are behind, the information on odometer allows active distraction for some distance beyond the
 * distraction object. This class listens to lane changes to update the relevant lane information. An instance of this class
 * should be shared among different instances of tasks which each can request the total level of distraction given the filter
 * each supplies to the {@link #getDistraction(BiFunction)} method.
 * <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 DistractionField implements EventListener
{

    /** GTU. */
    private final LaneBasedGtu gtu;

    /** Last time distractions were updated. */
    private Duration updateTime = null;

    /** Odometer values at distraction. */
    private final Map<RoadSideDistraction, Double> odos = new LinkedHashMap<>();

    /** Task demand per distraction. */
    private final Map<RoadSideDistraction, Double> taskDemands = new LinkedHashMap<>();

    /** Lanes and the applicable distractions. */
    private Map<RelativeLane, Set<RoadSideDistraction>> lanes = new LinkedHashMap<>();

    /**
     * Constructor.
     * @param gtu GTU
     */
    public DistractionField(final LaneBasedGtu gtu)
    {
        this.gtu = gtu;
        gtu.addListener(this, LaneBasedGtu.LANE_CHANGE_EVENT);
    }

    @Override
    public void notify(final Event event)
    {
        LateralDirectionality dir = LateralDirectionality.valueOf((String) ((Object[]) event.getContent())[1]);
        RelativeLane shift = new RelativeLane(dir, 1);
        Map<RelativeLane, Set<RoadSideDistraction>> newMap = new LinkedHashMap<>();
        this.lanes.entrySet().stream().forEach((e) -> newMap.put(e.getKey().add(shift), e.getValue()));
        this.lanes = newMap;
    }

    /**
     * Returns the level of distraction for the given direction.
     * @param filter filter to retain relevant distractions
     * @return level of distraction for the given direction
     * @throws ParameterException if parameter for lane structure is missing
     */
    public double getDistraction(final BiFunction<RelativeLane, RoadSideDistraction, Boolean> filter) throws ParameterException
    {
        perceiveDistractions();
        double taskDemand = 0.0;
        for (RelativeLane lane : this.lanes.keySet())
        {
            for (RoadSideDistraction distraction : this.lanes.get(lane))
            {
                if (filter.apply(lane, distraction))
                {
                    taskDemand += this.taskDemands.get(distraction);
                }
            }
        }
        return taskDemand;
    }

    /**
     * Caches odometer, lane and task demand for all distractions. Distractions are remembered when passed by their odometer.
     * Once the distraction results in a no-value task demand, it is removed as it is too far behind.
     * @throws ParameterException if parameter for lane structure is missing
     */
    private void perceiveDistractions() throws ParameterException
    {
        if (this.updateTime != null && this.gtu.getSimulator().getSimulatorTime().le(this.updateTime))
        {
            return;
        }
        this.updateTime = this.gtu.getSimulator().getSimulatorTime();

        // update odometer values and set lanes of all downstream distractions
        LaneStructure laneStructure = this.gtu.getTacticalPlanner().getPerception().getLaneStructure();
        double odo = this.gtu.getOdometer().si;
        for (RelativeLane lane : laneStructure.getRootCrossSection())
        {
            for (Entry<RoadSideDistraction> distraction : laneStructure.getDownstreamObjects(lane, RoadSideDistraction.class,
                    RelativePosition.FRONT, false))
            {
                this.odos.put(distraction.object(), odo + distraction.distance().si);
                this.lanes.computeIfAbsent(lane, (l) -> new LinkedHashSet<>()).add(distraction.object());
            }
        }

        // calculate distraction task demand and remove those without a value (indicating it is to far behind)
        Iterator<RoadSideDistraction> distractionIterator = this.odos.keySet().iterator();
        while (distractionIterator.hasNext())
        {
            RoadSideDistraction distraction = distractionIterator.next();
            OptionalDouble td = distraction.getDistraction(Length.ofSI(odo - this.odos.get(distraction)));
            if (td.isEmpty())
            {
                distractionIterator.remove();
                this.taskDemands.remove(distraction);
                this.lanes.values().stream().forEach((s) -> s.remove(distraction));
            }
            else
            {
                this.taskDemands.put(distraction, td.getAsDouble());
            }
        }

        // remove all lanes that no longer have any distraction
        Iterator<Set<RoadSideDistraction>> laneIterator = this.lanes.values().iterator();
        while (laneIterator.hasNext())
        {
            if (laneIterator.next().isEmpty())
            {
                laneIterator.remove();
            }
        }
    }

}