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ₖ &lt;
109      * {@code latestValidityDate} &lt; 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ₖ &lt;
186      * {@code earliestValidityDate} &lt; 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 }