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 }