DeadEndUtil.java

package org.opentrafficsim.road.gtu.tactical.util;

import java.util.SortedSet;

import org.djunits.value.vdouble.scalar.Acceleration;
import org.djunits.value.vdouble.scalar.Length;
import org.opentrafficsim.base.logger.Logger;
import org.opentrafficsim.base.parameters.ParameterException;
import org.opentrafficsim.base.parameters.ParameterTypes;
import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
import org.opentrafficsim.road.gtu.operational.SimpleOperationalPlan;
import org.opentrafficsim.road.gtu.perception.RelativeLane;
import org.opentrafficsim.road.gtu.perception.categories.InfrastructurePerception;
import org.opentrafficsim.road.gtu.tactical.TacticalContextEgo;
import org.opentrafficsim.road.network.LaneChangeInfo;

/**
 * Utility for tactical planners so they can deal with dead-ens situations.
 * <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 final class DeadEndUtil
{

    /**
     * Constructor.
     */
    private DeadEndUtil()
    {
        //
    }

    /**
     * Adjusts the simple operational plan to deal with dead-end situations.
     * @param context tactical information such as parameters and car-following model
     * @param plan simple operational plan
     * @return adjusted simple operational plan to deal with dead-end situations
     * @throws OperationalPlanException if there is no infrastructure perception
     * @throws ParameterException if a required parameter is not present
     */
    public static SimpleOperationalPlan dealWithDeadEnd(final TacticalContextEgo context, final SimpleOperationalPlan plan)
            throws OperationalPlanException, ParameterException
    {
        SimpleOperationalPlan simplePlan = plan;
        // deal with dead-end situations
        InfrastructurePerception infra = context.getPerception().getPerceptionCategory(InfrastructurePerception.class);
        SortedSet<LaneChangeInfo> lcInfo = infra.getPhysicalLaneChangeInfo(RelativeLane.CURRENT);
        if (!lcInfo.isEmpty())
        {
            Length remainingDist = lcInfo.first().remainingDistance();
            /*
             * Once the model has initiated a lane change, we no longer limit acceleration by the dead-end. This is because
             * either the lane change is instantaneous and the dead-end is no longer relevant, or because movement is required
             * for the lane change and we do not want the dead-end to keep the GTU in a stand-still dead-lock.
             */
            if (!simplePlan.isLaneChange())
            {
                boolean edge = context.getUnsafeGtu().getBookkeeping().isEdge();
                if (edge)
                {
                    int n = lcInfo.first().numberOfLaneChanges();
                    remainingDist = remainingDist.minus(context.getLength().times(n));
                }
                if (edge && remainingDist.lt0() && !context.getLaneChangeDirection().isNone())
                {
                    Logger.ots().info("Forced continuation of lane change for GTU {}.", context.getId());
                    /*
                     * We cannot allow the model to cancel a lane change so close to the dead-end. The model can decide when the
                     * lane change is acceptable to initiate, but once initiated there is no more space to cancel.
                     */
                    simplePlan = new SimpleOperationalPlan(simplePlan.getAcceleration(), simplePlan.getDuration(),
                            context.getLaneChangeDirection());
                }
                else
                {
                    Acceleration a = CarFollowingUtil.stop(context, remainingDist);
                    a = Acceleration.max(a, context.getParameters().getParameter(ParameterTypes.BCRIT).neg());
                    simplePlan.minimizeAcceleration(a);
                }
            }
            else if (remainingDist.lt0())
            {
                // change lane instantaneously
                context.getUnsafeGtu().changeLaneInstantaneously(simplePlan.getLaneChangeDirection());
                simplePlan = new SimpleOperationalPlan(simplePlan.getAcceleration(), simplePlan.getDuration());
            }
        }
        return simplePlan;
    }

}