1 /* Copyright 2002-2025 CS GROUP
2 * Licensed to CS GROUP (CS) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * CS licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.orekit.utils;
18
19 import java.util.function.Consumer;
20
21 import org.orekit.errors.OrekitException;
22 import org.orekit.errors.OrekitMessages;
23 import org.orekit.time.AbsoluteDate;
24 import org.orekit.time.TimeStamped;
25
26 /** Container for objects that apply to spans of time.
27 * <p>
28 * Time span maps can be seen either as an ordered collection of
29 * {@link Span time spans} or as an ordered collection
30 * of {@link Transition transitions}. Both views are dual one to
31 * each other. A time span extends from one transition to the
32 * next one, and a transition separates one time span from the
33 * next one. Each time span contains one entry that is valid during
34 * the time span; this entry may be null if nothing is valid during
35 * this time span.
36 * </p>
37 * <p>
38 * Typical uses of {@link TimeSpanMap} are to hold piecewise data, like for
39 * example an orbit count that changes at ascending nodes (in which case the
40 * entry would be an {@link Integer}), or a visibility status between several
41 * objects (in which case the entry would be a {@link Boolean}) or a drag
42 * coefficient that is expected to be estimated daily or three-hourly.
43 * </p>
44 * <p>
45 * Time span maps are built progressively. At first, they contain one
46 * {@link Span time span} only whose validity extends from past infinity to
47 * future infinity. Then new entries are added one at a time, associated with
48 * transition dates, in order to build up the complete map. The transition dates
49 * can be either the start of validity (when calling {@link #addValidAfter(Object,
50 * AbsoluteDate, boolean)}), or the end of the validity (when calling {@link
51 * #addValidBefore(Object, AbsoluteDate, boolean)}). Entries are often added at one
52 * end only (and mainly in chronological order), but this is not required. It is
53 * possible for example to first set up a map that cover a large range (say one day),
54 * and then to insert intermediate dates using for example propagation and event
55 * detectors to carve out some parts. This is akin to the way Binary Space Partitioning
56 * Trees work.
57 * </p>
58 * <p>
59 * Since 11.1, this class is thread-safe
60 * </p>
61 * @param <T> Type of the data.
62 * @author Luc Maisonobe
63 * @since 7.1
64 */
65 public class TimeSpanMap<T> {
66
67 /** Reference to last accessed data. */
68 private Span<T> current;
69
70 /** Number of time spans. */
71 private int nbSpans;
72
73 /** Create a map containing a single object, initially valid throughout the timeline.
74 * <p>
75 * The real validity of this first entry will be truncated as other
76 * entries are either {@link #addValidBefore(Object, AbsoluteDate, boolean)
77 * added before} it or {@link #addValidAfter(Object, AbsoluteDate, boolean)
78 * added after} it.
79 * </p>
80 * @param entry entry (initially valid throughout the timeline)
81 */
82 public TimeSpanMap(final T entry) {
83 current = new Span<>(entry);
84 nbSpans = 1;
85 }
86
87 /** Get the number of spans.
88 * <p>
89 * The number of spans is always at least 1. The number of transitions
90 * is always 1 less than the number of spans.
91 * </p>
92 * @return number of spans
93 * @since 11.1
94 */
95 public synchronized int getSpansNumber() {
96 return nbSpans;
97 }
98
99 /** Add an entry valid before a limit date.
100 * <p>
101 * As an entry is valid, it truncates or overrides the validity of the neighboring
102 * entries already present in the map.
103 * </p>
104 * <p>
105 * If the map already contains transitions that occur earlier than {@code latestValidityDate},
106 * the {@code erasesEarlier} parameter controls what to do with them. Lets consider
107 * the time span [tₖ ; tₖ₊₁[ associated with entry eₖ that would have been valid at time
108 * {@code latestValidityDate} prior to the call to the method (i.e. tₖ <
109 * {@code latestValidityDate} < tₖ₊₁).
110 * </p>
111 * <ul>
112 * <li>if {@code erasesEarlier} is {@code true}, then all earlier transitions
113 * up to and including tₖ are erased, and the {@code entry} will be valid from past infinity
114 * to {@code latestValidityDate}</li>
115 * <li>if {@code erasesEarlier} is {@code false}, then all earlier transitions
116 * are preserved, and the {@code entry} will be valid from tₖ
117 * to {@code latestValidityDate}</li>
118 * </ul>
119 * <p>
120 * In both cases, the existing entry eₖ time span will be truncated and will be valid
121 * only from {@code latestValidityDate} to tₖ₊₁.
122 * </p>
123 * @param entry entry to add
124 * @param latestValidityDate date before which the entry is valid
125 * @param erasesEarlier if true, the entry erases all existing transitions
126 * that are earlier than {@code latestValidityDate}
127 * @return span with added entry
128 * @since 11.1
129 */
130 public synchronized Span<T> addValidBefore(final T entry, final AbsoluteDate latestValidityDate, final boolean erasesEarlier) {
131
132 // update current reference to transition date
133 locate(latestValidityDate);
134
135 if (erasesEarlier) {
136
137 // drop everything before date
138 current.start = null;
139
140 // update count
141 nbSpans = 0;
142 for (Span<T> span = current; span != null; span = span.next()) {
143 ++nbSpans;
144 }
145
146 }
147
148 final Span<T> span = new Span<>(entry);
149
150 final Transition<T> start = current.getStartTransition();
151 if (start != null && start.getDate().equals(latestValidityDate)) {
152 // the transition at start of the current span is at the exact same date
153 // we update it, without adding a new transition
154 if (start.previous() != null) {
155 start.previous().setAfter(span);
156 }
157 start.setBefore(span);
158 } else {
159
160 if (current.getStartTransition() != null) {
161 current.getStartTransition().setAfter(span);
162 }
163
164 // we need to add a new transition somewhere inside the current span
165 insertTransition(latestValidityDate, span, current);
166
167 }
168
169 // we consider the last added transition as the new current one
170 current = span;
171
172 return span;
173
174 }
175
176 /** Add an entry valid after a limit date.
177 * <p>
178 * As an entry is valid, it truncates or overrides the validity of the neighboring
179 * entries already present in the map.
180 * </p>
181 * <p>
182 * If the map already contains transitions that occur later than {@code earliestValidityDate},
183 * the {@code erasesLater} parameter controls what to do with them. Lets consider
184 * the time span [tₖ ; tₖ₊₁[ associated with entry eₖ that would have been valid at time
185 * {@code earliestValidityDate} prior to the call to the method (i.e. tₖ <
186 * {@code earliestValidityDate} < tₖ₊₁).
187 * </p>
188 * <ul>
189 * <li>if {@code erasesLater} is {@code true}, then all later transitions
190 * from and including tₖ₊₁ are erased, and the {@code entry} will be valid from
191 * {@code earliestValidityDate} to future infinity</li>
192 * <li>if {@code erasesLater} is {@code false}, then all later transitions
193 * are preserved, and the {@code entry} will be valid from {@code earliestValidityDate}
194 * to tₖ₊₁</li>
195 * </ul>
196 * <p>
197 * In both cases, the existing entry eₖ time span will be truncated and will be valid
198 * only from tₖ to {@code earliestValidityDate}.
199 * </p>
200 * @param entry entry to add
201 * @param earliestValidityDate date after which the entry is valid
202 * @param erasesLater if true, the entry erases all existing transitions
203 * that are later than {@code earliestValidityDate}
204 * @return span with added entry
205 * @since 11.1
206 */
207 public synchronized Span<T> addValidAfter(final T entry, final AbsoluteDate earliestValidityDate, final boolean erasesLater) {
208
209 // update current reference to transition date
210 locate(earliestValidityDate);
211
212 if (erasesLater) {
213
214 // drop everything after date
215 current.end = null;
216
217 // update count
218 nbSpans = 0;
219 for (Span<T> span = current; span != null; span = span.previous()) {
220 ++nbSpans;
221 }
222
223 }
224
225 final Span<T> span = new Span<>(entry);
226 if (current.getEndTransition() != null) {
227 current.getEndTransition().setBefore(span);
228 }
229
230 final Transition<T> start = current.getStartTransition();
231 if (start != null && start.getDate().equals(earliestValidityDate)) {
232 // the transition at start of the current span is at the exact same date
233 // we update it, without adding a new transition
234 start.setAfter(span);
235 } else {
236 // we need to add a new transition somewhere inside the current span
237 insertTransition(earliestValidityDate, current, span);
238 }
239
240 // we consider the last added transition as the new current one
241 current = span;
242
243 return span;
244
245 }
246
247 /** Add an entry valid between two limit dates.
248 * <p>
249 * As an entry is valid, it truncates or overrides the validity of the neighboring
250 * entries already present in the map.
251 * </p>
252 * @param entry entry to add
253 * @param earliestValidityDate date after which the entry is valid
254 * @param latestValidityDate date before which the entry is valid
255 * @return span with added entry
256 * @since 11.1
257 */
258 public synchronized Span<T> addValidBetween(final T entry, final AbsoluteDate earliestValidityDate, final AbsoluteDate latestValidityDate) {
259
260 // handle special cases
261 if (AbsoluteDate.PAST_INFINITY.equals(earliestValidityDate)) {
262 if (AbsoluteDate.FUTURE_INFINITY.equals(latestValidityDate)) {
263 // we wipe everything in the map
264 current = new Span<>(entry);
265 return current;
266 } else {
267 // we wipe from past infinity
268 return addValidBefore(entry, latestValidityDate, true);
269 }
270 } else if (AbsoluteDate.FUTURE_INFINITY.equals(latestValidityDate)) {
271 // we wipe up to future infinity
272 return addValidAfter(entry, earliestValidityDate, true);
273 } else {
274
275 // locate spans at earliest and latest dates
276 locate(earliestValidityDate);
277 Span<T> latest = current;
278 while (latest.getEndTransition() != null && latest.getEnd().isBeforeOrEqualTo(latestValidityDate)) {
279 latest = latest.next();
280 --nbSpans;
281 }
282 if (latest == current) {
283 // the interval splits one transition in the middle, we need to duplicate the instance
284 latest = new Span<>(current.data);
285 if (current.getEndTransition() != null) {
286 current.getEndTransition().setBefore(latest);
287 }
288 }
289
290 final Span<T> span = new Span<>(entry);
291
292 // manage earliest transition
293 final Transition<T> start = current.getStartTransition();
294 if (start != null && start.getDate().equals(earliestValidityDate)) {
295 // the transition at start of the current span is at the exact same date
296 // we update it, without adding a new transition
297 start.setAfter(span);
298 } else {
299 // we need to add a new transition somewhere inside the current span
300 insertTransition(earliestValidityDate, current, span);
301 }
302
303 // manage latest transition
304 insertTransition(latestValidityDate, span, latest);
305
306 // we consider the last added transition as the new current one
307 current = span;
308
309 return span;
310
311 }
312
313 }
314
315 /** Get the entry valid at a specified date.
316 * <p>
317 * The expected complexity is O(1) for successive calls with
318 * neighboring dates, which is the more frequent use in propagation
319 * or orbit determination applications, and O(n) for random calls.
320 * </p>
321 * @param date date at which the entry must be valid
322 * @return valid entry at specified date
323 * @see #getSpan(AbsoluteDate)
324 */
325 public synchronized T get(final AbsoluteDate date) {
326 return getSpan(date).getData();
327 }
328
329 /** Get the time span containing a specified date.
330 * <p>
331 * The expected complexity is O(1) for successive calls with
332 * neighboring dates, which is the more frequent use in propagation
333 * or orbit determination applications, and O(n) for random calls.
334 * </p>
335 * @param date date belonging to the desired time span
336 * @return time span containing the specified date
337 * @since 9.3
338 */
339 public synchronized Span<T> getSpan(final AbsoluteDate date) {
340 locate(date);
341 return current;
342 }
343
344 /** Locate the time span containing a specified date.
345 * <p>
346 * The {@code current} field is updated to the located span.
347 * After the method returns, {@code current.getStartTransition()} is either
348 * null or its date is before or equal to date, and {@code
349 * current.getEndTransition()} is either null or its date is after date.
350 * </p>
351 * @param date date belonging to the desired time span
352 */
353 private synchronized void locate(final AbsoluteDate date) {
354
355 while (current.getStart().isAfter(date)) {
356 // current span is too late
357 current = current.previous();
358 }
359
360 while (current.getEnd().isBeforeOrEqualTo(date)) {
361
362 final Span<T> next = current.next();
363 if (next == null) {
364 // this happens when date is FUTURE_INFINITY
365 return;
366 }
367
368 // current span is too early
369 current = next;
370
371 }
372
373 }
374
375 /** Insert a transition.
376 * @param date transition date
377 * @param before span before transition
378 * @param after span after transition
379 * @since 11.1
380 */
381 private void insertTransition(final AbsoluteDate date, final Span<T> before, final Span<T> after) {
382 final Transition<T> transition = new Transition<>(this, date);
383 transition.setBefore(before);
384 transition.setAfter(after);
385 ++nbSpans;
386 }
387
388 /** Get the first (earliest) transition.
389 * @return first (earliest) transition, or null if there are no transitions
390 * @since 11.1
391 */
392 public synchronized Transition<T> getFirstTransition() {
393 return getFirstSpan().getEndTransition();
394 }
395
396 /** Get the last (latest) transition.
397 * @return last (latest) transition, or null if there are no transitions
398 * @since 11.1
399 */
400 public synchronized Transition<T> getLastTransition() {
401 return getLastSpan().getStartTransition();
402 }
403
404 /** Get the first (earliest) span.
405 * @return first (earliest) span
406 * @since 11.1
407 */
408 public synchronized Span<T> getFirstSpan() {
409 Span<T> span = current;
410 while (span.getStartTransition() != null) {
411 span = span.previous();
412 }
413 return span;
414 }
415
416 /** Get the first (earliest) span with non-null data.
417 * @return first (earliest) span with non-null data
418 * @since 12.1
419 */
420 public synchronized Span<T> getFirstNonNullSpan() {
421 Span<T> span = getFirstSpan();
422 while (span.getData() == null) {
423 if (span.getEndTransition() == null) {
424 throw new OrekitException(OrekitMessages.NO_CACHED_ENTRIES);
425 }
426 span = span.next();
427 }
428 return span;
429 }
430
431 /** Get the last (latest) span.
432 * @return last (latest) span
433 * @since 11.1
434 */
435 public synchronized Span<T> getLastSpan() {
436 Span<T> span = current;
437 while (span.getEndTransition() != null) {
438 span = span.next();
439 }
440 return span;
441 }
442
443 /** Get the last (latest) span with non-null data.
444 * @return last (latest) span with non-null data
445 * @since 12.1
446 */
447 public synchronized Span<T> getLastNonNullSpan() {
448 Span<T> span = getLastSpan();
449 while (span.getData() == null) {
450 if (span.getStartTransition() == null) {
451 throw new OrekitException(OrekitMessages.NO_CACHED_ENTRIES);
452 }
453 span = span.previous();
454 }
455 return span;
456 }
457
458 /** Extract a range of the map.
459 * <p>
460 * The object returned will be a new independent instance that will contain
461 * only the transitions that lie in the specified range.
462 * </p>
463 * <p>
464 * Consider for example a map containing objects O₀ valid before t₁, O₁ valid
465 * between t₁ and t₂, O₂ valid between t₂ and t₃, O₃ valid between t₃ and t₄,
466 * and O₄ valid after t₄. then calling this method with a {@code start}
467 * date between t₁ and t₂ and a {@code end} date between t₃ and t₄
468 * will result in a new map containing objects O₁ valid before t₂, O₂
469 * valid between t₂ and t₃, and O₃ valid after t₃. The validity of O₁
470 * is therefore extended in the past, and the validity of O₃ is extended
471 * in the future.
472 * </p>
473 * @param start earliest date at which a transition is included in the range
474 * (may be set to {@link AbsoluteDate#PAST_INFINITY} to keep all early transitions)
475 * @param end latest date at which a transition is included in the r
476 * (may be set to {@link AbsoluteDate#FUTURE_INFINITY} to keep all late transitions)
477 * @return a new instance with all transitions restricted to the specified range
478 * @since 9.2
479 */
480 public synchronized TimeSpanMap<T> extractRange(final AbsoluteDate start, final AbsoluteDate end) {
481
482 Span<T> span = getSpan(start);
483 final TimeSpanMap<T> range = new TimeSpanMap<>(span.getData());
484 while (span.getEndTransition() != null && span.getEndTransition().getDate().isBeforeOrEqualTo(end)) {
485 span = span.next();
486 range.addValidAfter(span.getData(), span.getStartTransition().getDate(), false);
487 }
488
489 return range;
490
491 }
492
493 /**
494 * Performs an action for each non-null element of map.
495 * <p>
496 * The action is performed chronologically.
497 * </p>
498 * @param action action to perform on the non-null elements
499 * @since 10.3
500 */
501 public synchronized void forEach(final Consumer<T> action) {
502 for (Span<T> span = getFirstSpan(); span != null; span = span.next()) {
503 if (span.getData() != null) {
504 action.accept(span.getData());
505 }
506 }
507 }
508
509 /** Class holding transition times.
510 * <p>
511 * This data type is dual to {@link Span}, it is
512 * focused on one transition date, and gives access to
513 * surrounding valid data whereas {@link Span} is focused
514 * on one valid data, and gives access to surrounding
515 * transition dates.
516 * </p>
517 * @param <S> Type of the data.
518 */
519 public static class Transition<S> implements TimeStamped {
520
521 /** Map this transition belongs to.
522 * @since 13.0
523 */
524 private final TimeSpanMap<S> map;
525
526 /** Transition date. */
527 private AbsoluteDate date;
528
529 /** Entry valid before the transition. */
530 private Span<S> before;
531
532 /** Entry valid after the transition. */
533 private Span<S> after;
534
535 /** Simple constructor.
536 * @param map map this transition belongs to
537 * @param date transition date
538 */
539 private Transition(final TimeSpanMap<S> map, final AbsoluteDate date) {
540 this.map = map;
541 this.date = date;
542 }
543
544 /** Set the span valid before transition.
545 * @param before span valid before transition (must be non-null)
546 */
547 void setBefore(final Span<S> before) {
548 this.before = before;
549 before.end = this;
550 }
551
552 /** Set the span valid after transition.
553 * @param after span valid after transition (must be non-null)
554 */
555 void setAfter(final Span<S> after) {
556 this.after = after;
557 after.start = this;
558 }
559
560 /** Get the transition date.
561 * @return transition date
562 */
563 @Override
564 public AbsoluteDate getDate() {
565 return date;
566 }
567
568 /** Move transition.
569 * <p>
570 * When moving a transition to past or future infinity, it will be disconnected
571 * from the time span it initially belonged to as the next or previous time
572 * span validity will be extends to infinity.
573 * </p>
574 * @param newDate new transition date
575 * @param eraseOverridden if true, spans that are entirely between current
576 * and new transition dates will be silently removed, if false and such
577 * spans exist, an exception will be triggered
578 * @since 13.0
579 */
580 public void resetDate(final AbsoluteDate newDate, final boolean eraseOverridden) {
581 if (newDate.isAfter(date)) {
582 // we are moving the transition towards future
583
584 // find span after new date
585 Span<S> newAfter = after;
586 while (newAfter.getEndTransition() != null &&
587 newAfter.getEndTransition().getDate().isBeforeOrEqualTo(newDate)) {
588 if (eraseOverridden) {
589 map.nbSpans--;
590 } else {
591 // forbidden collision detected
592 throw new OrekitException(OrekitMessages.TRANSITION_DATES_COLLISION,
593 date, newDate, newAfter.getEndTransition().getDate());
594 }
595 newAfter = newAfter.next();
596 }
597
598 synchronized (map) {
599 // perform update
600 date = newDate;
601 after = newAfter;
602 after.start = this;
603 map.current = before;
604
605 if (newDate.isInfinite()) {
606 // we have just moved the transition to future infinity, it should really disappear
607 map.nbSpans--;
608 before.end = null;
609 }
610 }
611
612 } else {
613 // we are moving transition towards past
614
615 // find span before new date
616 Span<S> newBefore = before;
617 while (newBefore.getStartTransition() != null &&
618 newBefore.getStartTransition().getDate().isAfterOrEqualTo(newDate)) {
619 if (eraseOverridden) {
620 map.nbSpans--;
621 } else {
622 // forbidden collision detected
623 throw new OrekitException(OrekitMessages.TRANSITION_DATES_COLLISION,
624 date, newDate, newBefore.getStartTransition().getDate());
625 }
626 newBefore = newBefore.previous();
627 }
628
629 synchronized (map) {
630 // perform update
631 date = newDate;
632 before = newBefore;
633 before.end = this;
634 map.current = after;
635
636 if (newDate.isInfinite()) {
637 // we have just moved the transition to past infinity, it should really disappear
638 map.nbSpans--;
639 after.start = null;
640 }
641 }
642
643 }
644 }
645
646 /** Get the previous transition.
647 * @return previous transition, or null if this transition was the first one
648 * @since 11.1
649 */
650 public Transition<S> previous() {
651 return before.getStartTransition();
652 }
653
654 /** Get the next transition.
655 * @return next transition, or null if this transition was the last one
656 * @since 11.1
657 */
658 public Transition<S> next() {
659 return after.getEndTransition();
660 }
661
662 /** Get the entry valid before transition.
663 * @return entry valid before transition
664 * @see #getSpanBefore()
665 */
666 public S getBefore() {
667 return before.getData();
668 }
669
670 /** Get the {@link Span} valid before transition.
671 * @return {@link Span} valid before transition
672 * @since 11.1
673 */
674 public Span<S> getSpanBefore() {
675 return before;
676 }
677
678 /** Get the entry valid after transition.
679 * @return entry valid after transition
680 * @see #getSpanAfter()
681 */
682 public S getAfter() {
683 return after.getData();
684 }
685
686 /** Get the {@link Span} valid after transition.
687 * @return {@link Span} valid after transition
688 * @since 11.1
689 */
690 public Span<S> getSpanAfter() {
691 return after;
692 }
693
694 }
695
696 /** Holder for one time span.
697 * <p>
698 * This data type is dual to {@link Transition}, it
699 * is focused on one valid data, and gives access to
700 * surrounding transition dates whereas {@link Transition}
701 * is focused on one transition date, and gives access to
702 * surrounding valid data.
703 * </p>
704 * @param <S> Type of the data.
705 * @since 9.3
706 */
707 public static class Span<S> {
708
709 /** Valid data. */
710 private final S data;
711
712 /** Start of validity for the data (null if span extends to past infinity). */
713 private Transition<S> start;
714
715 /** End of validity for the data (null if span extends to future infinity). */
716 private Transition<S> end;
717
718 /** Simple constructor.
719 * @param data valid data
720 */
721 private Span(final S data) {
722 this.data = data;
723 }
724
725 /** Get the data valid during this time span.
726 * @return data valid during this time span
727 */
728 public S getData() {
729 return data;
730 }
731
732 /** Get the previous time span.
733 * @return previous time span, or null if this time span was the first one
734 * @since 11.1
735 */
736 public Span<S> previous() {
737 return start == null ? null : start.getSpanBefore();
738 }
739
740 /** Get the next time span.
741 * @return next time span, or null if this time span was the last one
742 * @since 11.1
743 */
744 public Span<S> next() {
745 return end == null ? null : end.getSpanAfter();
746 }
747
748 /** Get the start of this time span.
749 * @return start of this time span (will be {@link AbsoluteDate#PAST_INFINITY}
750 * if {@link #getStartTransition()} returns null)
751 * @see #getStartTransition()
752 */
753 public AbsoluteDate getStart() {
754 return start == null ? AbsoluteDate.PAST_INFINITY : start.getDate();
755 }
756
757 /** Get the transition at start of this time span.
758 * @return transition at start of this time span (null if span extends to past infinity)
759 * @see #getStart()
760 * @since 11.1
761 */
762 public Transition<S> getStartTransition() {
763 return start;
764 }
765
766 /** Get the end of this time span.
767 * @return end of this time span (will be {@link AbsoluteDate#FUTURE_INFINITY}
768 * if {@link #getEndTransition()} returns null)
769 * @see #getEndTransition()
770 */
771 public AbsoluteDate getEnd() {
772 return end == null ? AbsoluteDate.FUTURE_INFINITY : end.getDate();
773 }
774
775 /** Get the transition at end of this time span.
776 * @return transition at end of this time span (null if span extends to future infinity)
777 * @see #getEnd()
778 * @since 11.1
779 */
780 public Transition<S> getEndTransition() {
781 return end;
782 }
783
784 }
785
786 }