View Javadoc
1   package org.opentrafficsim.road.network.speed;
2   
3   import java.util.ArrayList;
4   import java.util.LinkedHashMap;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.SortedSet;
8   import java.util.TreeSet;
9   
10  import org.djunits.unit.LengthUnit;
11  import org.djunits.value.vdouble.scalar.Acceleration;
12  import org.djunits.value.vdouble.scalar.Duration;
13  import org.djunits.value.vdouble.scalar.Length;
14  import org.djunits.value.vdouble.scalar.Speed;
15  import org.djutils.exceptions.Throw;
16  
17  /**
18   * Prospect of speed limits ahead, both legal and otherwise (e.g. curve, speed bump).
19   * <p>
20   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
21   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
22   * </p>
23   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
24   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
25   */
26  public class SpeedLimitProspect
27  {
28  
29      /** Spatial prospect of speed info. */
30      private final SortedSet<SpeedLimitEntry<?>> prospect = new TreeSet<>();
31  
32      /** Source objects for the speed info additions. */
33      private final Map<Object, SpeedLimitEntry<?>> addSources = new LinkedHashMap<>();
34  
35      /** Source objects for the speed info removals. */
36      private final Map<Object, SpeedLimitEntry<?>> removeSources = new LinkedHashMap<>();
37  
38      /** Last odometer value. */
39      private Length odometer;
40  
41      /**
42       * Constructor.
43       * @param odometer odometer value
44       */
45      public SpeedLimitProspect(final Length odometer)
46      {
47          this.odometer = odometer;
48      }
49  
50      /**
51       * Updates the distance values.
52       * @param newOdometer odometer value
53       */
54      public void update(final Length newOdometer)
55      {
56          Length dx = newOdometer.minus(this.odometer);
57          for (SpeedLimitEntry<?> entry : this.prospect)
58          {
59              entry.move(dx);
60          }
61          this.odometer = newOdometer;
62      }
63  
64      /**
65       * Returns whether the given source is already added in the prospect.
66       * @param source source
67       * @return whether the given source is already added in the prospect
68       */
69      public final boolean containsAddSource(final Object source)
70      {
71          return this.addSources.containsKey(source);
72      }
73  
74      /**
75       * Returns whether the given source is already removed in the prospect.
76       * @param source source
77       * @return whether the given source is already removed in the prospect
78       */
79      public final boolean containsRemoveSource(final Object source)
80      {
81          return this.removeSources.containsKey(source);
82      }
83  
84      /**
85       * Returns the odometer value at which the last update was performed.
86       * @return odometer value at which the last update was performed
87       */
88      public final Length getOdometer()
89      {
90          return this.odometer;
91      }
92  
93      /**
94       * Sets the speed info of a speed limit type.
95       * @param distance location to set info for a speed limit type
96       * @param speedLimitType speed limit type to set the info for
97       * @param speedInfo speed info to set
98       * @param source source object
99       * @param <T> class of speed info
100      * @throws IllegalStateException if speed info for a specific speed limit type is set or removed twice at the same distance
101      * @throws IllegalStateException if speed info for a specific speed limit type is set twice with negative distance
102      * @throws NullPointerException if any input is null
103      */
104     public final <T> void addSpeedInfo(final Length distance, final SpeedLimitType<T> speedLimitType, final T speedInfo,
105             final Object source)
106     {
107         Throw.whenNull(distance, "Distance may not be null.");
108         Throw.whenNull(speedLimitType, "Speed limit type may not be null.");
109         Throw.whenNull(speedInfo, "Speed info may not be null.");
110         checkAndAdd(new SpeedLimitEntry<>(distance, speedLimitType, speedInfo), source, false);
111     }
112 
113     /**
114      * Removes the speed info of a speed limit type.
115      * @param distance distance to remove speed info of a speed limit type
116      * @param speedLimitType speed limit type to remove speed info of
117      * @param source source object
118      * @throws IllegalStateException if speed info for a specific speed limit type is set or removed twice at the same distance
119      * @throws IllegalArgumentException if the speed limit type is {@code MAX_VEHICLE_SPEED}
120      * @throws IllegalArgumentException if the distance is negative
121      * @throws NullPointerException if any input is null
122      */
123     @SuppressWarnings({"unchecked", "rawtypes"})
124     public final void removeSpeedInfo(final Length distance, final SpeedLimitType<?> speedLimitType, final Object source)
125     {
126         Throw.whenNull(distance, "Distance may not be null.");
127         Throw.when(distance.si < 0, IllegalArgumentException.class,
128                 "Removing speed info in the past is not allowed. " + "Only add still active speed info.");
129         Throw.whenNull(speedLimitType, "Speed limit type may not be null.");
130         Throw.when(speedLimitType.equals(SpeedLimitTypes.MAX_VEHICLE_SPEED), IllegalArgumentException.class,
131                 "May not remove the maximum vehicle speed.");
132         // null value does not comply to being a T for SpeedLimitType<T> but is separately treated
133         checkAndAdd(new SpeedLimitEntry(distance, speedLimitType, null), source, true);
134     }
135 
136     /**
137      * Checks the speed limit entry before adding to the prospect.
138      * @param speedLimitEntry speed limit entry to add
139      * @param source source object
140      * @param remove whether the source causes a removal of info
141      * @throws IllegalStateException if the speed entry forms an undefined set with any existing entry
142      */
143     private void checkAndAdd(final SpeedLimitEntry<?> speedLimitEntry, final Object source, final boolean remove)
144     {
145         for (SpeedLimitEntry<?> s : this.prospect)
146         {
147             if (s.getSpeedLimitType().equals(speedLimitEntry.getSpeedLimitType()))
148             {
149                 /*
150                  * For entries at the same distance, the speed limit type may not be the same, this leaves us with an undefined
151                  * state as it cannot be derived which remains valid further on.
152                  */
153                 Throw.when(s.getDistance().equals(speedLimitEntry.getDistance()), IllegalStateException.class,
154                         "Info " + "of speed limit type '%s' is set twice at the same location (%s). This is undefined. "
155                                 + "Either remove speed info, or overwrite with new speed info.",
156                         s.getSpeedLimitType(), s.getDistance());
157             }
158         }
159         if (remove)
160         {
161             SpeedLimitEntry<?> prev = this.removeSources.get(source);
162             if (prev != null)
163             {
164                 this.prospect.remove(prev);
165             }
166             this.removeSources.put(source, speedLimitEntry);
167         }
168         else
169         {
170             SpeedLimitEntry<?> prev = this.addSources.get(source);
171             if (prev != null)
172             {
173                 this.prospect.remove(prev);
174             }
175             this.addSources.put(source, speedLimitEntry);
176         }
177         this.prospect.add(speedLimitEntry);
178     }
179 
180     /**
181      * Returns the distances at which a change in the prospect is present in order (upstream first). If multiple changes are
182      * present at the same distance, only one distance is returned in the list.
183      * @return distances at which a change in the prospect is present in order (upstream first)
184      */
185     public final List<Length> getDistances()
186     {
187         List<Length> list = new ArrayList<>();
188         for (SpeedLimitEntry<?> speedLimitEntry : this.prospect)
189         {
190             list.add(speedLimitEntry.getDistance());
191         }
192         return list;
193     }
194 
195     /**
196      * Returns the distances at which a change of the given speed limit type in the prospect is present in order (most upstream
197      * first). If multiple changes are present at the same distance, only one distance is returned in the list.
198      * @param speedLimitType speed limit type to get the distances of
199      * @return distances at which a change of the given speed limit type in the prospect is present in order
200      */
201     public final List<Length> getDistances(final SpeedLimitType<?> speedLimitType)
202     {
203         return getDistancesInRange(speedLimitType, null, null);
204     }
205 
206     /**
207      * Returns the upstream distances at which a change of the given speed limit type in the prospect is present in order (most
208      * upstream first). If multiple changes are present at the same distance, only one distance is returned in the list.
209      * @param speedLimitType speed limit type to get the distances of
210      * @return distances at which a change of the given speed limit type in the prospect is present in order
211      */
212     public final List<Length> getUpstreamDistances(final SpeedLimitType<?> speedLimitType)
213     {
214         return getDistancesInRange(speedLimitType, null, Length.ZERO);
215     }
216 
217     /**
218      * Returns the downstream distances at which a change of the given speed limit type in the prospect is present in order
219      * (most upstream first). If multiple changes are present at the same distance, only one distance is returned in the list.
220      * @param speedLimitType speed limit type to get the distances of
221      * @return distances at which a change of the given speed limit type in the prospect is present in order
222      */
223     public final List<Length> getDownstreamDistances(final SpeedLimitType<?> speedLimitType)
224     {
225         return getDistancesInRange(speedLimitType, Length.ZERO, null);
226     }
227 
228     /**
229      * Returns the distances between limits at which a change of the given speed limit type in the prospect is present in order
230      * (most upstream first). If multiple changes are present at the same distance, only one distance is returned in the list.
231      * @param speedLimitType speed limit type to get the distances of
232      * @param min minimum distance, may be {@code null} for no minimum limit
233      * @param max maximum distance, may be {@code null} for no maximum limit
234      * @return distances at which a change of the given speed limit type in the prospect is present in order
235      */
236     private List<Length> getDistancesInRange(final SpeedLimitType<?> speedLimitType, final Length min, final Length max)
237     {
238         List<Length> list = new ArrayList<>();
239         for (SpeedLimitEntry<?> speedLimitEntry : this.prospect)
240         {
241             if (speedLimitEntry.getSpeedLimitType().equals(speedLimitType)
242                     && (min == null || speedLimitEntry.getDistance().gt(min))
243                     && (max == null || speedLimitEntry.getDistance().le(max)))
244             {
245                 list.add(speedLimitEntry.getDistance());
246             }
247         }
248         return list;
249     }
250 
251     /**
252      * Returns whether the given speed limit type is changed at the given distance.
253      * @param distance distance to check
254      * @param speedLimitType speed limit type to check
255      * @return whether the given speed limit type is changed at the given distance
256      * @throws NullPointerException if distance is null
257      */
258     public final boolean speedInfoChanged(final Length distance, final SpeedLimitType<?> speedLimitType)
259     {
260         Throw.whenNull(distance, "Distance may not be null.");
261         for (SpeedLimitEntry<?> sle : this.prospect)
262         {
263             if (sle.getDistance().eq(distance) && sle.getSpeedLimitType().equals(speedLimitType))
264             {
265                 return true;
266             }
267         }
268         return false;
269     }
270 
271     /**
272      * Returns the speed info of given speed limit type where it changed. If the change was removing the speed limit type (e.g.
273      * end of corner), then {@code null} is returned.
274      * @param distance distance where the info changed
275      * @param speedLimitType speed limit type
276      * @return speed info of given speed limit type where it changed
277      * @throws IllegalArgumentException if the speed info did not change at the given distance for the speed limit type
278      * @param <T> class of the speed limit type info
279      */
280     public final <T> T getSpeedInfoChange(final Length distance, final SpeedLimitType<T> speedLimitType)
281     {
282         for (SpeedLimitEntry<?> sle : this.prospect)
283         {
284             if (sle.getDistance().eq(distance) && sle.getSpeedLimitType().equals(speedLimitType))
285             {
286                 @SuppressWarnings("unchecked")
287                 T info = (T) sle.getSpeedInfo();
288                 return info;
289             }
290         }
291         throw new IllegalArgumentException("Speed info of speed limit type '" + speedLimitType.getId()
292                 + "' is requested at a distance '" + distance + "' where the info is not changed.");
293     }
294 
295     /**
296      * Returns the speed info at a given location.
297      * @param distance where to get the speed info
298      * @return speed info at a given distance
299      * @throws NullPointerException if distance is null
300      */
301     public final SpeedLimitInfo getSpeedLimitInfo(final Length distance)
302     {
303         Throw.whenNull(distance, "Distance may not be null.");
304         SpeedLimitInfo speedLimitInfo = new SpeedLimitInfo();
305         for (SpeedLimitEntry<?> speedLimitEntry : this.prospect)
306         {
307             // use compareTo as this also determines order in this.prospect
308             if (speedLimitEntry.getDistance().compareTo(distance) > 0)
309             {
310                 // remaining entries are further ahead
311                 return speedLimitInfo;
312             }
313             // make appropriate change to speedLimitInfo
314             if (speedLimitEntry.getSpeedInfo() == null)
315             {
316                 speedLimitInfo.removeSpeedInfo(speedLimitEntry.getSpeedLimitType());
317             }
318             else
319             {
320                 // method addSpeedInfo guarantees that speedInfo in speedLimitEntry is T
321                 // for speedLimitType in speedLimitEntry is SpeedLimitType<T>, null is checked above
322                 setAsType(speedLimitInfo, speedLimitEntry);
323             }
324         }
325         return speedLimitInfo;
326     }
327 
328     /**
329      * Returns the speed info at a location following an acceleration over some duration.
330      * @param speed current speed
331      * @param acceleration acceleration to apply
332      * @param time duration of acceleration
333      * @return speed info at a given distance
334      * @throws NullPointerException if any input is null
335      */
336     public final SpeedLimitInfo getSpeedLimitInfo(final Speed speed, final Acceleration acceleration, final Duration time)
337     {
338         Throw.whenNull(speed, "Speed may not be null.");
339         Throw.whenNull(acceleration, "Acceleration may not be null.");
340         Throw.whenNull(time, "Time may not be null.");
341         return getSpeedLimitInfo(new Length(speed.si * time.si + .5 * acceleration.si * time.si * time.si, LengthUnit.SI));
342     }
343 
344     /**
345      * Sets speed info for a speed limit type in speed limit info by explicitly casting the types. From the context it should be
346      * certain that the speed info inside the speed limit entry matches the declared info type of the speed limit type inside
347      * the entry, i.e. {@code speedLimitEntry.getSpeedLimitType() = SpeedLimitType<T>} and
348      * {@code speedLimitEntry.getSpeedInfo() = T}.
349      * @param speedLimitInfo speed limit info to put speed info in
350      * @param speedLimitEntry entry with speed limit type and speed info to set
351      * @param <T> underlying speed info class depending on speed limit type
352      */
353     @SuppressWarnings("unchecked")
354     private <T> void setAsType(final SpeedLimitInfo speedLimitInfo, final SpeedLimitEntry<?> speedLimitEntry)
355     {
356         SpeedLimitType<T> speedLimitType = (SpeedLimitType<T>) speedLimitEntry.getSpeedLimitType();
357         T speedInfoOfType = (T) speedLimitEntry.getSpeedInfo();
358         speedLimitInfo.addSpeedInfo(speedLimitType, speedInfoOfType);
359     }
360 
361     /**
362      * Builds speed limit info with only MAX_VEHICLE_SPEED and the given speed limit type, where the speed info is obtained at
363      * the given distance.
364      * @param distance distance to get the speed info at
365      * @param speedLimitType speed limit type of which to include the info
366      * @param <T> class of speed info of given speed limit type
367      * @return speed limit info with only MAX_VEHICLE_SPEED and the given speed limit type
368      */
369     public final <T> SpeedLimitInfo buildSpeedLimitInfo(final Length distance, final SpeedLimitType<T> speedLimitType)
370     {
371         SpeedLimitInfo out = new SpeedLimitInfo();
372         out.addSpeedInfo(speedLimitType, getSpeedInfoChange(distance, speedLimitType));
373         for (SpeedLimitEntry<?> speedLimitEntry : this.prospect)
374         {
375             if (speedLimitEntry.getDistance().gt(distance))
376             {
377                 break;
378             }
379             if (speedLimitEntry.getSpeedLimitType().equals(SpeedLimitTypes.MAX_VEHICLE_SPEED))
380             {
381                 out.addSpeedInfo(SpeedLimitTypes.MAX_VEHICLE_SPEED,
382                         SpeedLimitTypes.MAX_VEHICLE_SPEED.getInfoClass().cast(speedLimitEntry.getSpeedInfo()));
383             }
384         }
385         return out;
386     }
387 
388     @Override
389     public final String toString()
390     {
391         StringBuilder stringBuilder = new StringBuilder("SpeedLimitProspect [");
392         String sep = "";
393         for (SpeedLimitEntry<?> sle : this.prospect)
394         {
395             stringBuilder.append(sep).append(sle.getDistance()).append(": ");
396             if (sle.getSpeedInfo() == null)
397             {
398                 stringBuilder.append(sle.getSpeedLimitType().getId()).append("=END");
399             }
400             else
401             {
402                 stringBuilder.append(sle.getSpeedLimitType().getId()).append("=");
403                 stringBuilder.append(sle.getSpeedInfo());
404             }
405             sep = ", ";
406         }
407         stringBuilder.append("]");
408         return stringBuilder.toString();
409     }
410 
411     /**
412      * Stores speed limit type and it's speed info with a location.
413      * <p>
414      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
415      * <br>
416      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
417      * </p>
418      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
419      * @param <T> class of speed info
420      */
421     private static class SpeedLimitEntry<T> implements Comparable<SpeedLimitEntry<?>>
422     {
423 
424         /** Location of the speed info. */
425         private Length distance;
426 
427         /** Speed limit type. */
428         private final SpeedLimitType<T> speedLimitType;
429 
430         /** Speed info. */
431         private final T speedInfo;
432 
433         /**
434          * Constructor.
435          * @param distance location of the speed info
436          * @param speedLimitType speed limit type
437          * @param speedInfo speed info
438          */
439         SpeedLimitEntry(final Length distance, final SpeedLimitType<T> speedLimitType, final T speedInfo)
440         {
441             this.distance = distance;
442             this.speedLimitType = speedLimitType;
443             this.speedInfo = speedInfo;
444         }
445 
446         /**
447          * Returns the location of the speed info.
448          * @return location of the speed info
449          */
450         public final Length getDistance()
451         {
452             return this.distance;
453         }
454 
455         /**
456          * Returns the speed limit type.
457          * @return speed limit type
458          */
459         public final SpeedLimitType<T> getSpeedLimitType()
460         {
461             return this.speedLimitType;
462         }
463 
464         /**
465          * Returns the speed info.
466          * @return the speed info
467          */
468         public final T getSpeedInfo()
469         {
470             return this.speedInfo;
471         }
472 
473         /**
474          * Move the record by a given distance.
475          * @param dist distance to move
476          */
477         public final void move(final Length dist)
478         {
479             this.distance = this.distance.minus(dist);
480         }
481 
482         @Override
483         public final int hashCode()
484         {
485             final int prime = 31;
486             int result = 1;
487             result = prime * result + this.distance.hashCode();
488             result = prime * result + this.speedInfo.hashCode();
489             result = prime * result + this.speedLimitType.hashCode();
490             return result;
491         }
492 
493         @Override
494         public final boolean equals(final Object obj)
495         {
496             if (this == obj)
497             {
498                 return true;
499             }
500             if (obj == null)
501             {
502                 return false;
503             }
504             if (getClass() != obj.getClass())
505             {
506                 return false;
507             }
508             SpeedLimitEntry<?> other = (SpeedLimitEntry<?>) obj;
509             if (!this.distance.equals(other.distance))
510             {
511                 return false;
512             }
513             if (!this.speedLimitType.equals(other.speedLimitType))
514             {
515                 return false;
516             }
517             if (this.speedInfo == null)
518             {
519                 if (other.speedInfo != null)
520                 {
521                     return false;
522                 }
523             }
524             else if (!this.speedInfo.equals(other.speedInfo))
525             {
526                 return false;
527             }
528             return true;
529         }
530 
531         @Override
532         public final int compareTo(final SpeedLimitEntry<?> speedLimitEntry)
533         {
534             if (this.equals(speedLimitEntry))
535             {
536                 return 0;
537             }
538             // order by distance
539             int comp = this.distance.compareTo(speedLimitEntry.distance);
540             if (comp != 0)
541             {
542                 return comp;
543             }
544             // order by speed limit type
545             comp = this.speedLimitType.getId().compareTo(speedLimitEntry.speedLimitType.getId());
546             if (comp != 0)
547             {
548                 return comp;
549             }
550             // equal distance and speed limit type is not allowed, so below code is not used
551             // if this requirement changes, compareTo should still work
552             if (this.speedInfo == null)
553             {
554                 if (speedLimitEntry.speedInfo == null)
555                 {
556                     return 0; // both null
557                 }
558                 return -1; // null under non-null
559             }
560             else if (speedLimitEntry.speedInfo == null)
561             {
562                 return 1; // non-null over null
563             }
564             return this.speedInfo.hashCode() < speedLimitEntry.speedInfo.hashCode() ? -1 : 1;
565         }
566 
567         @Override
568         public final String toString()
569         {
570             return "SpeedLimitEntry [distance=" + this.distance + ", speedLimitType=" + this.speedLimitType + ", speedInfo="
571                     + this.speedInfo + "]";
572         }
573 
574     }
575 
576 }