LaneSpeedLimits.java

package org.opentrafficsim.road.network.speed;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.TreeMap;

import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Speed;
import org.djutils.exceptions.Throw;
import org.opentrafficsim.core.gtu.GtuType;

/**
 * Speed limits for the context of a single lane.
 * <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 LaneSpeedLimits
{

    /** Speed limits that are applicable to GTU types. */
    private final Map<GtuType, Speed> gtuTypeSpeedLimits;

    /** Speed limit information for time windows. */
    private NavigableMap<Duration, LocalSpeedLimit> speedLimits = new TreeMap<>();

    /**
     * Constructor with no speed limit.
     */
    public LaneSpeedLimits()
    {
        this.gtuTypeSpeedLimits = new LinkedHashMap<>();
    }

    /**
     * Constructor with only a fixed speed limit.
     * @param speedLimit overall speed limit
     */
    public LaneSpeedLimits(final Speed speedLimit)
    {
        this.gtuTypeSpeedLimits = new LinkedHashMap<>();
        addSpeedLimit(speedLimit);
    }

    /**
     * Constructor with only GTU type speed limits.
     * @param gtuTypeSpeedLimits GTU type speed limits
     */
    public LaneSpeedLimits(final Map<GtuType, Speed> gtuTypeSpeedLimits)
    {
        Throw.whenNull(gtuTypeSpeedLimits, "gtuTypeSpeedLimits");
        this.gtuTypeSpeedLimits = new LinkedHashMap<>(gtuTypeSpeedLimits);
    }

    /**
     * Constructor with overall speed limit and GTU type speed limits.
     * @param speedLimit overall speed limit
     * @param gtuTypeSpeedLimits GTU type speed limits
     */
    public LaneSpeedLimits(final Speed speedLimit, final Map<GtuType, Speed> gtuTypeSpeedLimits)
    {
        Throw.whenNull(gtuTypeSpeedLimits, "gtuTypeSpeedLimits");
        this.gtuTypeSpeedLimits = new LinkedHashMap<>(gtuTypeSpeedLimits);
        addSpeedLimit(speedLimit);
    }

    /**
     * Add non-enforced constant speed limit.
     * @param speed speed limit
     * @return this object for method chaining
     */
    public LaneSpeedLimits addSpeedLimit(final Speed speed)
    {
        Throw.whenNull(speed, "speed");
        return addSpeedLimit(Duration.ZERO, speed, false, false);
    }

    /**
     * Add constant speed limit.
     * <p>
     * If enforcement is aware of the GTU type, any speed limit applicable to the GTU type (e.g. 80km/h for trucks) becomes an
     * enforced speed limit.
     * @param speed speed limit
     * @param enforced whether the speed limit is enforced
     * @param enforcedGtuTypeAware whether the speed limit enforcement is aware of GTU types
     * @return this object for method chaining
     */
    public LaneSpeedLimits addSpeedLimit(final Speed speed, final boolean enforced, final boolean enforcedGtuTypeAware)
    {
        Throw.whenNull(speed, "speed");
        return addSpeedLimit(Duration.ZERO, speed, enforced, enforcedGtuTypeAware);
    }

    /**
     * Add non-enforced speed limit. Time of day will by cycled through. For example, if a speed limit of 100km/h is added at 6h
     * and a speed limit of 130km/h is added at 19h, 100km/h applies from 06:00 till 19:00, while 130km/h applies for all other
     * hours, including between 00:00 and 06:00.
     * @param timeOfDay time-of-day when the speed limit becomes active
     * @param speed speed of the speed limit
     * @return this object for method chaining
     * @throws IllegalArgumentException when time-of-day is not in the range [0 24) hours
     */
    public LaneSpeedLimits addSpeedLimit(final Duration timeOfDay, final Speed speed)
    {
        Throw.whenNull(timeOfDay, "timeOfDay");
        Throw.whenNull(speed, "speed");
        return addSpeedLimit(timeOfDay, speed, false, false);
    }

    /**
     * Add speed limit. Time of day will by cycled through. For example, if a speed limit of 100km/h is added at 6h and a speed
     * limit of 130km/h is added at 19h, 100km/h applies from 06:00 till 19:00, while 130km/h applies for all other hours,
     * including between 00:00 and 06:00.
     * <p>
     * If enforcement is aware of the GTU type, any speed limit applicable to the GTU type (e.g. 80km/h for trucks) becomes an
     * enforced speed limit.
     * @param timeOfDay time-of-day when the speed limit becomes active
     * @param speed speed of the speed limit
     * @param enforced whether the speed limit is enforced
     * @param enforcedGtuTypeAware whether the speed limit enforcement is aware of GTU types
     * @return this object for method chaining
     * @throws IllegalArgumentException when time-of-day is not in the range [0 24) hours
     */
    public LaneSpeedLimits addSpeedLimit(final Duration timeOfDay, final Speed speed, final boolean enforced,
            final boolean enforcedGtuTypeAware)
    {
        Throw.whenNull(timeOfDay, "timeOfDay");
        Throw.whenNull(speed, "speed");
        Throw.when(timeOfDay.si < 0.0 || timeOfDay.si >= 86400.0, IllegalArgumentException.class,
                "Time of day value must be between 0 (inclusive) and 24 (exclusive) hours.");
        this.speedLimits.put(timeOfDay, new LocalSpeedLimit(new SpeedLimit(speed, enforced), enforcedGtuTypeAware));
        return this;
    }

    /**
     * Returns the speed limit.
     * @param timeOfDay time-of-day
     * @return speed limit, empty if no speed limit given
     */
    public Optional<SpeedLimit> getSpeedLimit(final Duration timeOfDay)
    {
        LocalSpeedLimit localSpeedLimit = getLocalSpeedLimit(timeOfDay);
        return Optional.ofNullable(localSpeedLimit == null ? null : localSpeedLimit.speedLimit());
    }

    /**
     * Returns the applicable speed limits for the given GTU type and time-of-day.
     * @param gtuType GTU type
     * @param timeOfDay time-of-day
     * @return applicable speed limits for the given GTU type and time-of-day
     */
    public SpeedLimits getSpeedLimits(final GtuType gtuType, final Duration timeOfDay)
    {
        LocalSpeedLimit localSpeedLimit = getLocalSpeedLimit(timeOfDay);
        Speed gtuTypeSpeed = getGtuTypeSpeedLimit(gtuType);
        if (gtuTypeSpeed != null)
        {
            return new SpeedLimits(localSpeedLimit == null ? null : localSpeedLimit.speedLimit(),
                    new SpeedLimit(gtuTypeSpeed, localSpeedLimit != null && localSpeedLimit.enforcedGtuTypeAware()
                            && localSpeedLimit.speedLimit().enforced()));
        }
        return new SpeedLimits(localSpeedLimit == null ? null : localSpeedLimit.speedLimit(), null);
    }

    /**
     * Returns the local speed limit at the given time-of-day.
     * @param timeOfDay time-of-day
     * @return local speed limit at the given time-of-day
     */
    private LocalSpeedLimit getLocalSpeedLimit(final Duration timeOfDay)
    {
        Entry<Duration, LocalSpeedLimit> todEntry = this.speedLimits.floorEntry(timeOfDay);
        // if there is information but not before the requested time-of-day, return the last information (cycles through day)
        if (todEntry == null)
        {
            if (!this.speedLimits.isEmpty())
            {
                return this.speedLimits.lastEntry().getValue();
            }
            else
            {
                return null;
            }
        }
        else
        {
            return todEntry.getValue();
        }
    }

    /**
     * Returns the speed limit for the GTU type, or any of its parents.
     * @param gtuType GTU type
     * @return speed limit for the GTU type, or any of its parents
     */
    private Speed getGtuTypeSpeedLimit(final GtuType gtuType)
    {
        Speed speed = this.gtuTypeSpeedLimits.get(gtuType);
        if (speed != null)
        {
            return speed;
        }
        Optional<GtuType> parent = gtuType.getParent();
        if (!parent.isPresent())
        {
            this.gtuTypeSpeedLimits.put(gtuType, null);
            return null;
        }
        return getGtuTypeSpeedLimit(parent.get());
    }

    /**
     * Record of speed limit information.
     * @param speedLimit speed limit
     * @param enforcedGtuTypeAware whether the enforcement is GTU type aware
     */
    private record LocalSpeedLimit(SpeedLimit speedLimit, boolean enforcedGtuTypeAware)
    {
    }

}