StripeData.java

package org.opentrafficsim.road.network.lane;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.djunits.value.vdouble.scalar.Length;
import org.djutils.exceptions.Throw;
import org.opentrafficsim.base.StripeElement;
import org.opentrafficsim.base.StripeElement.StripeLateralSync;
import org.opentrafficsim.core.gtu.GtuType;
import org.opentrafficsim.core.network.LateralDirectionality;

/**
 * Container for data about stripes, independent from the link and curvature.
 * <p>
 * Copyright (c) 2024-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/wjschakel">Wouter Schakel</a>
 */
public class StripeData
{

    /** Stripe elements. */
    private List<StripeElement> elements;

    /** Left permeability. */
    private final boolean left;

    /** Right permeability. */
    private final boolean right;

    /** Lateral permeability per GTU type and direction. */
    private final Map<GtuType, Set<LateralDirectionality>> permeabilityMap = new LinkedHashMap<>();

    /** Lateral synchronization. */
    private StripeLateralSync lateralSync = StripeLateralSync.LINK;

    /** Phase synchronization. */
    private StripePhaseSync phaseSync = StripePhaseSync.NONE;

    /** Period based on all stripe elements. */
    private Double period;

    /**
     * Constructor.
     * @param elements list of stripe elements
     * @param left left overall permeability
     * @param right right overall permeability
     */
    public StripeData(final List<StripeElement> elements, final boolean left, final boolean right)
    {
        this.elements = elements;
        this.left = left;
        this.right = right;
    }

    /**
     * Returns the elements.
     * @return elements
     */
    public List<StripeElement> getElements()
    {
        return this.elements;
    }
    
    /**
     * Sets the elements.
     * @param elements elements
     */
    public void setElements(final List<StripeElement> elements)
    {
        this.elements = elements;
        this.period = null;
    }

    /**
     * Add lateral permeability for a GTU type. This overrules overall stripe permeability. Add NONE to prevent lane changes.
     * Add both LEFT and RIGHT in two calls, to enable lane changes. Add LEFT or RIGHT to enable one direction while prohibiting
     * the other.
     * @param gtuType GTU type to add permeability for
     * @param lateralDirection direction to add compared to the direction of the design line
     */
    public void addPermeability(final GtuType gtuType, final LateralDirectionality lateralDirection)
    {
        if (!this.permeabilityMap.containsKey(gtuType))
        {
            this.permeabilityMap.put(gtuType, new LinkedHashSet<LateralDirectionality>(2));
        }
        this.permeabilityMap.get(gtuType).add(lateralDirection);
    }

    /**
     * Returns whether the given GTU type is allowed to cross the line in the given lateral direction.
     * @param gtuType GTU type to look for.
     * @param lateralDirection direction to look for (LEFT or RIGHT) compared to the direction of the design line.
     * @return whether the road marker is permeable for the GTU type.
     */
    public final boolean isPermeable(final GtuType gtuType, final LateralDirectionality lateralDirection)
    {
        Throw.when(lateralDirection.isNone(), RuntimeException.class,
                "May not request NONE lateral direction for permeability.");
        for (GtuType testGtuType = gtuType; null != testGtuType; testGtuType = testGtuType.getParent())
        {
            Set<LateralDirectionality> set = this.permeabilityMap.get(testGtuType);
            if (null != set)
            {
                return set.contains(lateralDirection);
            }
        }
        return lateralDirection.isLeft() ? this.left : this.right;
    }

    /**
     * Sets the lateral synchronization.
     * @param lateralSync lateral synchronization
     */
    public void setLateralSync(final StripeLateralSync lateralSync)
    {
        this.lateralSync = lateralSync;
    }

    /**
     * Returns the lateral synchronization.
     * @return lateral synchronization
     */
    public StripeLateralSync getLateralSync()
    {
        return this.lateralSync;
    }

    /**
     * Sets the phase synchronization.
     * @param phaseSync phase synchronization
     */
    public void setPhaseSync(final StripePhaseSync phaseSync)
    {
        this.phaseSync = phaseSync;
    }

    /**
     * Returns the phase synchronization.
     * @return phase synchronization
     */
    public StripePhaseSync getPhaseSync()
    {
        return this.phaseSync;
    }

    /**
     * Returns the period of the common dash pattern.
     * @return period of the common dash pattern
     */
    public double getPeriod()
    {
        if (this.period == null)
        {
            this.period = getPeriod(this.elements);
        }
        return this.period;
    }
    
    /**
     * Returns the period after which the given line gap-dash patterns repeat as a whole. Lengths are rounded to a precision of
     * 0.0001 to find the greatest common divisor.
     * @param elements elements
     * @return period
     */
    public static double getPeriod(final List<StripeElement> elements)
    {
        List<Double> lineLengths = new ArrayList<>();
        for (StripeElement element : elements)
        {
            if (element.dashes() != null)
            {
                double length = 0.0;
                for (Length gapDash : element.dashes())
                {
                    length += gapDash.si;
                }
                lineLengths.add(length);
            }
        }
        return getPeriod(lineLengths);
    }

    /**
     * Returns the period after which the given line gap-dash patterns repeat as a whole. Lengths are rounded to a precision of
     * 0.0001 to find the greatest common divisor.
     * @param lineLengths gap-dash pattern lengths
     * @return period
     */
    private static double getPeriod(final Collection<Double> lineLengths)
    {
        Set<Double> set = new LinkedHashSet<>(lineLengths);
        if (lineLengths.isEmpty())
        {
            return -1.0;
        }
        else if (set.size() == 1)
        {
            return ((long) (lineLengths.iterator().next() * 10000)) / 10000.0;
        }
        long gcd = 1L;
        for (double length : set)
        {
            gcd = BigInteger.valueOf(gcd).gcd(BigInteger.valueOf((long) (length * 10000))).longValue();
        }
        return gcd / 10000.0;
    }
    
    /**
     * Returns the width, which is the sum of stripe elements.
     * @return width
     */
    public Length getWidth()
    {
        Length width = Length.ZERO;
        for (StripeElement element : getElements())
        {
            width = width.plus(element.width());
        }
        return width;
    }

    /**
     * Method of stripe phase synchronization.
     */
    public enum StripePhaseSync
    {
        /** Do not synchronize. */
        NONE(false),

        /** Synchronize phase to upstream stripe. */
        UPSTREAM(true),

        /** Synchronize phase to downstream stripe. */
        DOWNSTREAM(true);

        /** Whether synchronization should be applied. */
        private final boolean sync;

        /**
         * Constructor.
         * @param sync whether synchronization should be applied
         */
        StripePhaseSync(final boolean sync)
        {
            this.sync = sync;
        }

        /**
         * Returns whether synchronization should be applied.
         * @return whether synchronization should be applied
         */
        public boolean isSync()
        {
            return this.sync;
        }
    }

}