OperationalPlan.java

  1. package org.opentrafficsim.core.gtu.plan.operational;

  2. import java.io.Serializable;
  3. import java.util.Arrays;

  4. import org.djunits.value.vdouble.scalar.Acceleration;
  5. import org.djunits.value.vdouble.scalar.Duration;
  6. import org.djunits.value.vdouble.scalar.Length;
  7. import org.djunits.value.vdouble.scalar.Speed;
  8. import org.djunits.value.vdouble.scalar.Time;
  9. import org.djutils.draw.point.OrientedPoint2d;
  10. import org.djutils.draw.point.Point2d;
  11. import org.djutils.exceptions.Throw;
  12. import org.djutils.exceptions.Try;
  13. import org.djutils.immutablecollections.ImmutableList;
  14. import org.opentrafficsim.core.geometry.OtsGeometryException;
  15. import org.opentrafficsim.core.geometry.OtsLine2d;
  16. import org.opentrafficsim.core.gtu.Gtu;
  17. import org.opentrafficsim.core.gtu.RelativePosition;

  18. /**
  19.  * An Operational plan describes a path through the world with a speed profile that a GTU intends to follow. The OperationalPlan
  20.  * can be updated or replaced at any time (including before it has been totally executed), for which a tactical planner is
  21.  * responsible. The operational plan is implemented using segments of the movement (time, location, speed, acceleration) that
  22.  * the GTU will use to plan its location and movement. Within an OperationalPlan the GTU cannot reverse direction along the path
  23.  * of movement. This ensures that the timeAtDistance method will never have to select among several valid solutions.
  24.  * <p>
  25.  * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  26.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  27.  * </p>
  28.  * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  29.  * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  30.  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  31.  */
  32. public class OperationalPlan implements Serializable
  33. {
  34.     /** */
  35.     private static final long serialVersionUID = 20151114L;

  36.     /** The path to follow from a certain time till a certain time. */
  37.     private final OtsLine2d path;

  38.     /** The absolute start time when we start executing the path. */
  39.     private final Time startTime;

  40.     /** The segments that make up the path with an acceleration, constant speed or deceleration profile. */
  41.     private final Segments segments;

  42.     /** The duration of executing the entire operational plan. */
  43.     private final Duration totalDuration;

  44.     /** The length of the entire operational plan. */
  45.     private final Length totalLength;

  46.     /** GTU for debugging purposes. */
  47.     private final Gtu gtu;

  48.     /**
  49.      * An array of relative start times of each segment, expressed in the SI unit, where the last time is the overall ending
  50.      * time of the operational plan.
  51.      */
  52.     private final double[] segmentStartDurations;

  53.     /**
  54.      * An array of relative start distances of each segment, expressed in the SI unit, where the last distance is the overall
  55.      * ending distance of the operational plan.
  56.      */
  57.     private final double[] segmentStartDistances;

  58.     /** The drifting speed. Speeds under this value will be cropped to zero. */
  59.     public static final double DRIFTING_SPEED_SI = 1E-3;

  60.     /**
  61.      * Creates a stand-still plan at a point. A 1m path in the direction of the point is created.
  62.      * @param gtu Gtu; GTU.
  63.      * @param point OrientedPoint2d; point.
  64.      * @param startTime Time; start time.
  65.      * @param duration Duration; duration.
  66.      * @return OperationalPlan; stand-still plan.
  67.      */
  68.     public static OperationalPlan standStill(final Gtu gtu, final OrientedPoint2d point, final Time startTime,
  69.             final Duration duration)
  70.     {
  71.         Point2d p2 = new Point2d(point.x + Math.cos(point.getDirZ()), point.y + Math.sin(point.getDirZ()));
  72.         OtsLine2d path = Try.assign(() -> new OtsLine2d(point, p2), "Unexpected geometry exception.");
  73.         return new OperationalPlan(gtu, path, startTime, Segments.standStill(duration));
  74.     }

  75.     /**
  76.      * Construct an operational plan. The plan will be as long as the minimum of the path or segments allow.
  77.      * @param gtu Gtu; the GTU for debugging purposes
  78.      * @param path OtsLine2d; the path to follow from a certain time till a certain time. The path should have &lt;i&gt;at
  79.      *            least&lt;/i&gt; the length
  80.      * @param startTime Time; the absolute start time when we start executing the path
  81.      * @param segments Segments; the segments that make up the longitudinal dynamics
  82.      */
  83.     public OperationalPlan(final Gtu gtu, final OtsLine2d path, final Time startTime, final Segments segments)
  84.     {
  85.         this.gtu = gtu;
  86.         this.startTime = startTime;
  87.         this.segments = segments;
  88.         this.segmentStartDurations = new double[this.segments.size() + 1];
  89.         this.segmentStartDistances = new double[this.segments.size() + 1];

  90.         Length pathLength = path.getLength();
  91.         Duration segmentsDuration = Duration.ZERO;
  92.         Length segmentsLength = Length.ZERO;
  93.         for (int i = 0; i < this.segments.size(); i++)
  94.         {
  95.             this.segmentStartDurations[i] = segmentsDuration.si;
  96.             this.segmentStartDistances[i] = segmentsLength.si;
  97.             Segment segment = this.segments.get(i);
  98.             segmentsDuration = segmentsDuration.plus(segment.duration());
  99.             segmentsLength = segmentsLength.plus(segment.totalDistance());
  100.         }
  101.         this.segmentStartDurations[this.segments.size()] = segmentsDuration.si;
  102.         this.segmentStartDistances[this.segments.size()] = segmentsLength.si;

  103.         // If segmentsLength == 0, we have a stand-still plan with non-zero length path. This path is required as a degenerate
  104.         // OtsLine2d (with <2 points) is not allowed. In that case (in else) do not truncate path.
  105.         if (segmentsLength.gt0() && pathLength.gt(segmentsLength))
  106.         {
  107.             this.totalDuration = segmentsDuration;
  108.             this.totalLength = segmentsLength;
  109.             this.path = Try.assign(() -> path.extract(0.0, this.totalLength.si), "Unexpected path truncation exception.");
  110.         }
  111.         else if (segmentsLength.gt(pathLength))
  112.         {
  113.             this.totalLength = pathLength;
  114.             int i = this.segments.size();
  115.             while (i > 1 && this.segmentStartDistances[i - 1] > pathLength.si)
  116.             {
  117.                 i--;
  118.             }
  119.             double distanceInLast = this.totalLength.si - this.segmentStartDistances[i - 1];
  120.             Duration timeInLast = this.segments.get(i - 1).durationAtDistance(Length.instantiateSI(distanceInLast));
  121.             this.totalDuration = Duration.instantiateSI(timeInLast.si + this.segmentStartDurations[i - 1]);
  122.             this.path = path;
  123.         }
  124.         else
  125.         {
  126.             this.totalDuration = segmentsDuration;
  127.             this.totalLength = segmentsLength;
  128.             this.path = path;
  129.         }
  130.     }

  131.     /**
  132.      * Return the path that will be traveled. If the plan is a wait plan, the start point of the path is good; the end point of
  133.      * the path is bogus (should only be used to determine the orientation of the GTU).
  134.      * @return OtsLine2d; the path
  135.      */
  136.     public OtsLine2d getPath()
  137.     {
  138.         return this.path;
  139.     }

  140.     /**
  141.      * Return the (absolute) start time of the operational plan.
  142.      * @return Time; startTime
  143.      */
  144.     public Time getStartTime()
  145.     {
  146.         return this.startTime;
  147.     }

  148.     /**
  149.      * Return the start speed of the entire plan.
  150.      * @return Speed; startSpeed
  151.      */
  152.     public Speed getStartSpeed()
  153.     {
  154.         return this.segments.get(0).startSpeed();
  155.     }

  156.     /**
  157.      * Return the segments (parts with constant speed, acceleration or deceleration) of the operational plan.
  158.      * @return ImmutableList&lt;OperationalPlan.Segment&gt;; segmentList
  159.      */
  160.     public ImmutableList<Segment> getOperationalPlanSegmentList()
  161.     {
  162.         return this.segments.getSegments();
  163.     }

  164.     /**
  165.      * Return the time it will take to complete the entire operational plan.
  166.      * @return Duration; the time it will take to complete the entire operational plan
  167.      */
  168.     public Duration getTotalDuration()
  169.     {
  170.         return this.totalDuration;
  171.     }

  172.     /**
  173.      * Return the distance the entire operational plan will cover.
  174.      * @return Length; the distance of the entire operational plan
  175.      */
  176.     public Length getTotalLength()
  177.     {
  178.         return this.totalLength;
  179.     }

  180.     /**
  181.      * Return the time it will take to complete the entire operational plan.
  182.      * @return Time; the time it will take to complete the entire operational plan
  183.      */
  184.     public Time getEndTime()
  185.     {
  186.         return this.startTime.plus(this.totalDuration);
  187.     }

  188.     /**
  189.      * Provide the end location of this operational plan as a DirectedPoint.
  190.      * @return OrientedPoint2d; the end location
  191.      */
  192.     public OrientedPoint2d getEndLocation()
  193.     {
  194.         return Try.assign(() -> this.path.getLocationFraction(Math.min(1.0, this.totalLength.si / this.path.getLength().si)),
  195.                 "Unexpected exception for path extraction till 1.0.");
  196.     }

  197.     /**
  198.      * Returns the index of the segment covering the given time.
  199.      * @param time Time; time.
  200.      * @return int; index of the segment covering the given time.
  201.      */
  202.     private int getSegment(final Time time)
  203.     {
  204.         double duration = time.si - this.startTime.si;
  205.         int segment = 0;
  206.         while (segment < this.segments.size() - 1 && this.segmentStartDurations[segment + 1] < duration)
  207.         {
  208.             segment++;
  209.         }
  210.         return segment;
  211.     }

  212.     /**
  213.      * Return the time when the GTU will reach the given distance.
  214.      * @param distance Length; the distance to calculate the time for
  215.      * @return the time it will take to have traveled the given distance
  216.      */
  217.     public final Time timeAtDistance(final Length distance)
  218.     {
  219.         Throw.when(getTotalLength().lt(distance), IllegalArgumentException.class, "Requesting %s from a plan with length %s",
  220.                 distance, getTotalLength());
  221.         int segment = 0;
  222.         while (segment < this.segments.size() && this.segmentStartDistances[segment + 1] < distance.si)
  223.         {
  224.             segment++;
  225.         }
  226.         Duration durationInSegment = this.segments.get(segment)
  227.                 .durationAtDistance(Length.instantiateSI(distance.si - this.segmentStartDistances[segment]));
  228.         return Time.instantiateSI(this.startTime.si + this.segmentStartDurations[segment] + durationInSegment.si);
  229.     }

  230.     /**
  231.      * Calculate the location after the given duration since the start of the plan.
  232.      * @param duration Duration; the relative time to look for a location
  233.      * @return the location after the given duration since the start of the plan.
  234.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  235.      */
  236.     public final OrientedPoint2d getLocation(final Duration duration) throws OperationalPlanException
  237.     {
  238.         return getLocation(this.startTime.plus(duration));
  239.     }

  240.     /**
  241.      * Calculate the location at the given time.
  242.      * @param time Time; the absolute time to look for a location
  243.      * @return the location at the given time.
  244.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  245.      */
  246.     public final OrientedPoint2d getLocation(final Time time) throws OperationalPlanException
  247.     {
  248.         Throw.when(time.lt(this.startTime), OperationalPlanException.class, "Requested time is before start time.");
  249.         Throw.when(time.gt(this.getEndTime()), OperationalPlanException.class, "Requested time is beyond end time.");
  250.         double fraction = this.totalLength.eq0() ? 0.0 : getTraveledDistance(time).si / this.totalLength.si;
  251.         return Try.assign(() -> this.path.getLocationFraction(fraction, 0.01), OperationalPlanException.class,
  252.                 "Unable to derive location for time.");
  253.     }

  254.     /**
  255.      * Calculate the location after the given duration since the start of the plan.
  256.      * @param time Time; the relative time to look for a location
  257.      * @param pos RelativePosition; relative position
  258.      * @return the location after the given duration since the start of the plan.
  259.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  260.      */
  261.     public final OrientedPoint2d getLocation(final Time time, final RelativePosition pos) throws OperationalPlanException
  262.     {
  263.         double distanceSI = getTraveledDistance(time).si + pos.dx().si;
  264.         return this.path.getLocationExtendedSI(distanceSI);
  265.     }

  266.     /**
  267.      * Calculate the speed of the GTU after the given duration since the start of the plan.
  268.      * @param time Duration; the relative time to look for a location
  269.      * @return the location after the given duration since the start of the plan.
  270.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  271.      */
  272.     public final Speed getSpeed(final Duration time) throws OperationalPlanException
  273.     {
  274.         return getSpeed(time.plus(this.startTime));
  275.     }

  276.     /**
  277.      * Calculate the speed of the GTU at the given time.
  278.      * @param time Time; the absolute time to look for a location
  279.      * @return the location at the given time.
  280.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  281.      */
  282.     public final Speed getSpeed(final Time time) throws OperationalPlanException
  283.     {
  284.         int segment = getSegment(time);
  285.         Duration durationInSegment = Duration.instantiateSI(time.si - this.startTime.si - this.segmentStartDurations[segment]);
  286.         durationInSegment = fixDoublePrecision(durationInSegment, segment);
  287.         return this.segments.get(segment).speed(durationInSegment);
  288.     }

  289.     /**
  290.      * Maximize to segment duration in case of double precision issue.
  291.      * @param durationInSegment Duration; duration in segment.
  292.      * @param segment int; segment number.
  293.      * @return duration in segment, maximized to segment duration if beyond within 1e-6.
  294.      */
  295.     private Duration fixDoublePrecision(final Duration durationInSegment, final int segment)
  296.     {
  297.         if (this.segments.get(segment).duration().lt(durationInSegment)
  298.                 && durationInSegment.si - this.segments.get(segment).duration().si < 1e-6)
  299.         {
  300.             return this.segments.get(segment).duration();
  301.         }
  302.         return durationInSegment;
  303.     }

  304.     /**
  305.      * Calculate the acceleration of the GTU after the given duration since the start of the plan.
  306.      * @param time Duration; the relative time to look for a location
  307.      * @return the location after the given duration since the start of the plan.
  308.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  309.      */
  310.     public final Acceleration getAcceleration(final Duration time) throws OperationalPlanException
  311.     {
  312.         return getAcceleration(time.plus(this.startTime));
  313.     }

  314.     /**
  315.      * Calculate the acceleration of the GTU at the given time.
  316.      * @param time Time; the absolute time to look for a location
  317.      * @return the location at the given time.
  318.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  319.      */
  320.     public final Acceleration getAcceleration(final Time time) throws OperationalPlanException
  321.     {
  322.         return this.segments.get(getSegment(time)).acceleration();
  323.     }

  324.     /**
  325.      * Calculate the distance traveled as part of this plan after the given duration since the start of the plan.
  326.      * @param duration Duration; the relative time to calculate the traveled distance
  327.      * @return the distance traveled as part of this plan after the given duration since the start of the plan.
  328.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  329.      */
  330.     public final Length getTraveledDistance(final Duration duration) throws OperationalPlanException
  331.     {
  332.         return getTraveledDistance(this.startTime.plus(duration));
  333.     }

  334.     /**
  335.      * Calculate the distance traveled as part of this plan at the given absolute time.
  336.      * @param time Time; the absolute time to calculate the traveled distance for as part of this plan
  337.      * @return the distance traveled as part of this plan at the given time
  338.      * @throws OperationalPlanException when the time is after the validity of the operational plan
  339.      */
  340.     public Length getTraveledDistance(final Time time) throws OperationalPlanException
  341.     {
  342.         Throw.when(time.lt(this.getStartTime()), OperationalPlanException.class,
  343.                 "getTravelDistance exception: requested traveled distance before start of plan");
  344.         Throw.when(time.si > this.getEndTime().si + 1e-6, OperationalPlanException.class,
  345.                 "getTravelDistance exception: requested traveled distance beyond end of plan");
  346.         int segment = getSegment(time);
  347.         Duration durationInSegment = Duration.instantiateSI(time.si - this.startTime.si - this.segmentStartDurations[segment]);
  348.         durationInSegment = fixDoublePrecision(durationInSegment, segment);
  349.         double distanceInSegment = this.segments.get(segment).distance(durationInSegment).si;
  350.         return Length.instantiateSI(this.segmentStartDistances[segment] + distanceInSegment);
  351.     }

  352.     /**
  353.      * Calculates when the GTU will be at the given point. The point does not need to be at the traveled path, as the point is
  354.      * projected to the path at 90 degrees. The point may for instance be the end of a lane, which is crossed by a GTU possibly
  355.      * during a lane change.
  356.      * @param point OrientedPoint2d; point with angle, which will be projected to the path at 90 degrees
  357.      * @param upstream boolean; true if the point is upstream of the path
  358.      * @return Time; time at point
  359.      */
  360.     public final Time timeAtPoint(final OrientedPoint2d point, final boolean upstream)
  361.     {
  362.         Point2d p1 = point;
  363.         // point at 90 degrees
  364.         Point2d p2 = new Point2d(point.x - Math.sin(point.getDirZ()), point.y + Math.cos(point.getDirZ()));
  365.         double traveledDistanceAlongPath = 0.0;
  366.         try
  367.         {
  368.             if (upstream)
  369.             {
  370.                 Point2d p = Point2d.intersectionOfLines(this.path.get(0), this.path.get(1), p1, p2);
  371.                 double dist = traveledDistanceAlongPath - this.path.get(0).distance(p);
  372.                 dist = dist >= 0.0 ? dist : 0.0; // negative in case of a gap
  373.                 return timeAtDistance(Length.instantiateSI(dist));
  374.             }
  375.             for (int i = 0; i < this.path.size() - 1; i++)
  376.             {
  377.                 Point2d prevPoint = this.path.get(i);
  378.                 Point2d nextPoint = this.path.get(i + 1);
  379.                 Point2d p = Point2d.intersectionOfLines(prevPoint, nextPoint, p1, p2);
  380.                 if (p == null)
  381.                 {
  382.                     // point too close, check next section
  383.                     continue;
  384.                 }
  385.                 boolean onSegment =
  386.                         prevPoint.distance(nextPoint) + 2e-5 > Math.max(prevPoint.distance(p), nextPoint.distance(p));
  387.                 if (p != null // on segment, or last segment
  388.                         && (i == this.path.size() - 2 || onSegment))
  389.                 {
  390.                     // point is on the line
  391.                     traveledDistanceAlongPath += this.path.get(i).distance(p);
  392.                     if (traveledDistanceAlongPath > this.path.getLength().si)
  393.                     {
  394.                         return Time.instantiateSI(Double.NaN);
  395.                     }
  396.                     return timeAtDistance(Length.instantiateSI(traveledDistanceAlongPath));
  397.                 }
  398.                 else
  399.                 {
  400.                     traveledDistanceAlongPath += this.path.get(i).distance(this.path.get(i + 1));
  401.                 }
  402.             }
  403.         }
  404.         catch (OtsGeometryException exception)
  405.         {
  406.             throw new RuntimeException("Index out of bounds on projection of point to path of operational plan", exception);
  407.         }
  408.         this.gtu.getSimulator().getLogger().always().error("timeAtPoint failed");
  409.         return null;
  410.     }

  411.     /** {@inheritDoc} */
  412.     @SuppressWarnings("checkstyle:designforextension")
  413.     @Override
  414.     public int hashCode()
  415.     {
  416.         final int prime = 31;
  417.         int result = 1;
  418.         result = prime * result + ((this.segments == null) ? 0 : this.segments.hashCode());
  419.         result = prime * result + ((this.path == null) ? 0 : this.path.hashCode());
  420.         result = prime * result + ((this.startTime == null) ? 0 : this.startTime.hashCode());
  421.         return result;
  422.     }

  423.     /** {@inheritDoc} */
  424.     @SuppressWarnings({"checkstyle:needbraces", "checkstyle:designforextension"})
  425.     @Override
  426.     public boolean equals(final Object obj)
  427.     {
  428.         if (this == obj)
  429.             return true;
  430.         if (obj == null)
  431.             return false;
  432.         if (getClass() != obj.getClass())
  433.             return false;
  434.         OperationalPlan other = (OperationalPlan) obj;
  435.         if (this.segments == null)
  436.         {
  437.             if (other.segments != null)
  438.                 return false;
  439.         }
  440.         else if (!this.segments.equals(other.segments))
  441.             return false;
  442.         if (this.path == null)
  443.         {
  444.             if (other.path != null)
  445.                 return false;
  446.         }
  447.         else if (!this.path.equals(other.path))
  448.             return false;
  449.         if (this.startTime == null)
  450.         {
  451.             if (other.startTime != null)
  452.                 return false;
  453.         }
  454.         else if (!this.startTime.equals(other.startTime))
  455.             return false;
  456.         return true;
  457.     }

  458.     /** {@inheritDoc} */
  459.     @SuppressWarnings("checkstyle:designforextension")
  460.     @Override
  461.     public String toString()
  462.     {
  463.         return "OperationalPlan [path=" + this.path + ", startTime=" + this.startTime + ", segments=" + this.segments
  464.                 + ", totalDuration=" + this.totalDuration + ", segmentStartTimesSI="
  465.                 + Arrays.toString(this.segmentStartDurations) + "]";
  466.     }

  467. }