GenericTimeStampedCache.java

  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. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.concurrent.atomic.AtomicInteger;
  21. import java.util.concurrent.atomic.AtomicLong;
  22. import java.util.concurrent.atomic.AtomicReference;
  23. import java.util.concurrent.locks.ReadWriteLock;
  24. import java.util.concurrent.locks.ReentrantReadWriteLock;
  25. import java.util.stream.Stream;

  26. import org.hipparchus.exception.LocalizedCoreFormats;
  27. import org.hipparchus.util.FastMath;
  28. import org.orekit.errors.OrekitException;
  29. import org.orekit.errors.OrekitIllegalArgumentException;
  30. import org.orekit.errors.OrekitIllegalStateException;
  31. import org.orekit.errors.OrekitMessages;
  32. import org.orekit.errors.TimeStampedCacheException;
  33. import org.orekit.time.AbsoluteDate;
  34. import org.orekit.time.TimeStamped;

  35. /** Generic thread-safe cache for {@link TimeStamped time-stamped} data.

  36.  * @param <T> Type of the cached data.

  37.  * @author Luc Maisonobe
  38.  */
  39. public class GenericTimeStampedCache<T extends TimeStamped> implements TimeStampedCache<T> {

  40.     /** Default number of independent cached time slots. */
  41.     public static final int DEFAULT_CACHED_SLOTS_NUMBER = 10;

  42.     /** Quantum step. */
  43.     private static final double QUANTUM_STEP = 1.0e-6;

  44.     /** Reference date for indexing. */
  45.     private final AtomicReference<AbsoluteDate> reference;

  46.     /** Maximum number of independent cached time slots. */
  47.     private final int maxSlots;

  48.     /** Maximum duration span in seconds of one slot. */
  49.     private final double maxSpan;

  50.     /** Quantum gap above which a new slot is created instead of extending an existing one. */
  51.     private final long newSlotQuantumGap;

  52.     /** Generator to use for yet non-cached data. */
  53.     private final TimeStampedGenerator<T> generator;

  54.     /** Overriding mean step. */
  55.     private final double overridingMeanStep;

  56.     /** Maximum number of entries in a neighbors array. */
  57.     private final int maxNeighborsSize;

  58.     /** Independent time slots cached. */
  59.     private final List<Slot> slots;

  60.     /** Number of calls to the getNeighbors method. */
  61.     private final AtomicInteger getNeighborsCalls;

  62.     /** Number of calls to the generate method. */
  63.     private final AtomicInteger generateCalls;

  64.     /** Number of evictions. */
  65.     private final AtomicInteger evictions;

  66.     /** Global lock. */
  67.     private final ReadWriteLock lock;

  68.     /** Simple constructor.
  69.      * @param maxNeighborsSize maximum size of the arrays to be returned by {@link
  70.      * #getNeighbors(AbsoluteDate, int)}, must be at least 2
  71.      * @param maxSlots maximum number of independent cached time slots
  72.      * @param maxSpan maximum duration span in seconds of one slot
  73.      * (can be set to {@code Double.POSITIVE_INFINITY} if desired)
  74.      * @param newSlotInterval time interval above which a new slot is created
  75.      * instead of extending an existing one
  76.      * @param generator generator to use for yet non-existent data
  77.      */
  78.     public GenericTimeStampedCache(final int maxNeighborsSize, final int maxSlots, final double maxSpan,
  79.                                    final double newSlotInterval, final TimeStampedGenerator<T> generator) {
  80.         this(maxNeighborsSize, maxSlots, maxSpan, newSlotInterval, generator, Double.NaN);

  81.     }

  82.     /** Simple constructor with overriding minimum step.
  83.      * @param maxNeighborsSize maximum size of the arrays to be returned by {@link
  84.      * #getNeighbors(AbsoluteDate, int)}, must be at least 2
  85.      * @param maxSlots maximum number of independent cached time slots
  86.      * @param maxSpan maximum duration span in seconds of one slot
  87.      * (can be set to {@code Double.POSITIVE_INFINITY} if desired)
  88.      * @param newSlotInterval time interval above which a new slot is created
  89.      * instead of extending an existing one
  90.      * @param generator generator to use for yet non-existent data
  91.      * @param overridingMeanStep overriding mean step designed for non-homogeneous tabulated values. To be used for example
  92.      *                    when caching monthly tabulated values. Use {@code Double.NaN} otherwise.
  93.      * @throws OrekitIllegalArgumentException if :
  94.      * <ul>
  95.      *     <li>neighbors size &lt; 2 </li>
  96.      *     <li>maximum allowed number of slots &lt; 1</li>
  97.      *     <li>minimum step ≤ 0 </li>
  98.      * </ul>
  99.      */
  100.     public GenericTimeStampedCache(final int maxNeighborsSize, final int maxSlots, final double maxSpan,
  101.                                    final double newSlotInterval, final TimeStampedGenerator<T> generator,
  102.                                    final double overridingMeanStep) {

  103.         // safety check
  104.         if (maxSlots < 1) {
  105.             throw new OrekitIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL, maxSlots, 1);
  106.         }
  107.         if (overridingMeanStep <= 0) {
  108.             throw new OrekitIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL_BOUND_EXCLUDED,
  109.                                                      overridingMeanStep, 0);
  110.         }
  111.         if (maxNeighborsSize < 2) {
  112.             throw new OrekitIllegalArgumentException(OrekitMessages.NOT_ENOUGH_CACHED_NEIGHBORS, maxNeighborsSize, 2);
  113.         }

  114.         this.reference          = new AtomicReference<>();
  115.         this.maxSlots           = maxSlots;
  116.         this.maxSpan            = maxSpan;
  117.         this.newSlotQuantumGap  = FastMath.round(newSlotInterval / QUANTUM_STEP);
  118.         this.generator          = generator;
  119.         this.overridingMeanStep = overridingMeanStep;
  120.         this.maxNeighborsSize   = maxNeighborsSize;
  121.         this.slots              = new ArrayList<>(maxSlots);
  122.         this.getNeighborsCalls  = new AtomicInteger(0);
  123.         this.generateCalls      = new AtomicInteger(0);
  124.         this.evictions          = new AtomicInteger(0);
  125.         this.lock               = new ReentrantReadWriteLock();

  126.     }

  127.     /** Get the generator.
  128.      * @return generator
  129.      */
  130.     public TimeStampedGenerator<T> getGenerator() {
  131.         return generator;
  132.     }

  133.     /** Get the maximum number of independent cached time slots.
  134.      * @return maximum number of independent cached time slots
  135.      */
  136.     public int getMaxSlots() {
  137.         return maxSlots;
  138.     }

  139.     /** Get the maximum duration span in seconds of one slot.
  140.      * @return maximum duration span in seconds of one slot
  141.      */
  142.     public double getMaxSpan() {
  143.         return maxSpan;
  144.     }

  145.     /** Get quantum gap above which a new slot is created instead of extending an existing one.
  146.      * <p>
  147.      * The quantum gap is the {@code newSlotInterval} value provided at construction
  148.      * rounded to the nearest quantum step used internally by the cache.
  149.      * </p>
  150.      * @return quantum gap in seconds
  151.      */
  152.     public double getNewSlotQuantumGap() {
  153.         return newSlotQuantumGap * QUANTUM_STEP;
  154.     }

  155.     /** Get the number of calls to the {@link #getNeighbors(AbsoluteDate)} method.
  156.      * <p>
  157.      * This number of calls is used as a reference to interpret {@link #getGenerateCalls()}.
  158.      * </p>
  159.      * @return number of calls to the {@link #getNeighbors(AbsoluteDate)} method
  160.      * @see #getGenerateCalls()
  161.      */
  162.     public int getGetNeighborsCalls() {
  163.         return getNeighborsCalls.get();
  164.     }

  165.     /** Get the number of calls to the generate method.
  166.      * <p>
  167.      * This number of calls is related to the number of cache misses and may
  168.      * be used to tune the cache configuration. Each cache miss implies at
  169.      * least one call is performed, but may require several calls if the new
  170.      * date is far offset from the existing cache, depending on the number of
  171.      * elements and step between elements in the arrays returned by the generator.
  172.      * </p>
  173.      * @return number of calls to the generate method
  174.      * @see #getGetNeighborsCalls()
  175.      */
  176.     public int getGenerateCalls() {
  177.         return generateCalls.get();
  178.     }

  179.     /** Get the number of slots evictions.
  180.      * <p>
  181.      * This number should remain small when the max number of slots is sufficient
  182.      * with respect to the number of concurrent requests to the cache. If it
  183.      * increases too much, then the cache configuration is probably bad and cache
  184.      * does not really improve things (in this case, the {@link #getGenerateCalls()
  185.      * number of calls to the generate method} will probably increase too.
  186.      * </p>
  187.      * @return number of slots evictions
  188.      */
  189.     public int getSlotsEvictions() {
  190.         return evictions.get();
  191.     }

  192.     /** Get the number of slots in use.
  193.      * @return number of slots in use
  194.      */
  195.     public int getSlots() {

  196.         lock.readLock().lock();
  197.         try {
  198.             return slots.size();
  199.         } finally {
  200.             lock.readLock().unlock();
  201.         }

  202.     }

  203.     /** Get the total number of entries cached.
  204.      * @return total number of entries cached
  205.      */
  206.     public int getEntries() {

  207.         lock.readLock().lock();
  208.         try {
  209.             int entries = 0;
  210.             for (final Slot slot : slots) {
  211.                 entries += slot.getEntries();
  212.             }
  213.             return entries;
  214.         } finally {
  215.             lock.readLock().unlock();
  216.         }

  217.     }

  218.     /** {@inheritDoc} */
  219.     @Override
  220.     public T getEarliest() throws IllegalStateException {

  221.         lock.readLock().lock();
  222.         try {
  223.             if (slots.isEmpty()) {
  224.                 throw new OrekitIllegalStateException(OrekitMessages.NO_CACHED_ENTRIES);
  225.             }
  226.             return slots.get(0).getEarliest();
  227.         } finally {
  228.             lock.readLock().unlock();
  229.         }

  230.     }

  231.     /** {@inheritDoc} */
  232.     @Override
  233.     public T getLatest() throws IllegalStateException {

  234.         lock.readLock().lock();
  235.         try {
  236.             if (slots.isEmpty()) {
  237.                 throw new OrekitIllegalStateException(OrekitMessages.NO_CACHED_ENTRIES);
  238.             }
  239.             return slots.get(slots.size() - 1).getLatest();
  240.         } finally {
  241.             lock.readLock().unlock();
  242.         }

  243.     }

  244.     /** {@inheritDoc} */
  245.     @Override
  246.     public int getMaxNeighborsSize() {
  247.         return maxNeighborsSize;
  248.     }

  249.     /** {@inheritDoc} */
  250.     @Override
  251.     public Stream<T> getNeighbors(final AbsoluteDate central, final int n) {

  252.         if (n > maxNeighborsSize) {
  253.             throw new OrekitException(OrekitMessages.NOT_ENOUGH_DATA, maxNeighborsSize);
  254.         }

  255.         lock.readLock().lock();
  256.         try {
  257.             getNeighborsCalls.incrementAndGet();
  258.             final long dateQuantum = quantum(central);
  259.             return selectSlot(central, dateQuantum).getNeighbors(central, dateQuantum, n);
  260.         } finally {
  261.             lock.readLock().unlock();
  262.         }

  263.     }

  264.     /** Convert a date to a rough global quantum.
  265.      * <p>
  266.      * We own a global read lock while calling this method.
  267.      * </p>
  268.      * @param date date to convert
  269.      * @return quantum corresponding to the date
  270.      */
  271.     private long quantum(final AbsoluteDate date) {
  272.         reference.compareAndSet(null, date);
  273.         return FastMath.round(date.durationFrom(reference.get()) / QUANTUM_STEP);
  274.     }

  275.     /** Select a slot containing a date.
  276.      * <p>
  277.      * We own a global read lock while calling this method.
  278.      * </p>
  279.      * @param date target date
  280.      * @param dateQuantum global quantum of the date
  281.      * @return slot covering the date
  282.      */
  283.     private Slot selectSlot(final AbsoluteDate date, final long dateQuantum) {

  284.         Slot selected = null;

  285.         int index = slots.isEmpty() ? 0 : slotIndex(dateQuantum);
  286.         if (slots.isEmpty() ||
  287.             slots.get(index).getEarliestQuantum() > dateQuantum + newSlotQuantumGap ||
  288.             slots.get(index).getLatestQuantum()   < dateQuantum - newSlotQuantumGap) {
  289.             // no existing slot is suitable

  290.             // upgrade the read lock to a write lock so we can change the list of available slots
  291.             lock.readLock().unlock();
  292.             lock.writeLock().lock();

  293.             try {
  294.                 // check slots again as another thread may have changed
  295.                 // the list while we were waiting for the write lock
  296.                 index = slots.isEmpty() ? 0 : slotIndex(dateQuantum);
  297.                 if (slots.isEmpty() ||
  298.                     slots.get(index).getEarliestQuantum() > dateQuantum + newSlotQuantumGap ||
  299.                     slots.get(index).getLatestQuantum()   < dateQuantum - newSlotQuantumGap) {

  300.                     // we really need to create a new slot in the current thread
  301.                     // (no other threads have created it while we were waiting for the lock)
  302.                     if (!slots.isEmpty() &&
  303.                         slots.get(index).getLatestQuantum() < dateQuantum - newSlotQuantumGap) {
  304.                         ++index;
  305.                     }

  306.                     if (slots.size() >= maxSlots) {
  307.                         // we must prevent exceeding allowed max

  308.                         // select the oldest accessed slot for eviction
  309.                         int evict = 0;
  310.                         for (int i = 0; i < slots.size(); ++i) {
  311.                             if (slots.get(i).getLastAccess() < slots.get(evict).getLastAccess()) {
  312.                                 evict = i;
  313.                             }
  314.                         }

  315.                         // evict the selected slot
  316.                         evictions.incrementAndGet();
  317.                         slots.remove(evict);

  318.                         if (evict < index) {
  319.                             // adjust index of created slot as it was shifted by the eviction
  320.                             index--;
  321.                         }
  322.                     }

  323.                     slots.add(index, new Slot(date));

  324.                 }

  325.             } finally {
  326.                 // downgrade back to a read lock
  327.                 lock.readLock().lock();
  328.                 lock.writeLock().unlock();
  329.             }
  330.         }

  331.         selected = slots.get(index);


  332.         return selected;

  333.     }

  334.     /** Get the index of the slot in which a date could be cached.
  335.      * <p>
  336.      * We own a global read lock while calling this method.
  337.      * </p>
  338.      * @param dateQuantum quantum of the date to search for
  339.      * @return the slot in which the date could be cached
  340.      */
  341.     private int slotIndex(final long dateQuantum) {

  342.         int  iInf = 0;
  343.         final long qInf = slots.get(iInf).getEarliestQuantum();
  344.         int  iSup = slots.size() - 1;
  345.         final long qSup = slots.get(iSup).getLatestQuantum();
  346.         while (iSup - iInf > 0) {
  347.             final int iInterp = (int) ((iInf * (qSup - dateQuantum) + iSup * (dateQuantum - qInf)) / (qSup - qInf));
  348.             final int iMed    = FastMath.max(iInf, FastMath.min(iInterp, iSup));
  349.             final Slot slot   = slots.get(iMed);
  350.             if (dateQuantum < slot.getEarliestQuantum()) {
  351.                 iSup = iMed - 1;
  352.             } else if (dateQuantum > slot.getLatestQuantum()) {
  353.                 iInf = FastMath.min(iSup, iMed + 1);
  354.             } else {
  355.                 return iMed;
  356.             }
  357.         }

  358.         return iInf;

  359.     }

  360.     /** Time slot. */
  361.     private final class Slot {

  362.         /** Cached time-stamped entries. */
  363.         private final List<Entry> cache;

  364.         /** Earliest quantum. */
  365.         private AtomicLong earliestQuantum;

  366.         /** Latest quantum. */
  367.         private AtomicLong latestQuantum;

  368.         /** Index from a previous recent call. */
  369.         private AtomicInteger guessedIndex;

  370.         /** Last access time. */
  371.         private AtomicLong lastAccess;

  372.         /** Simple constructor.
  373.          * @param date central date for initial entries to insert in the slot
  374.          */
  375.         Slot(final AbsoluteDate date) {

  376.             // allocate cache
  377.             this.cache = new ArrayList<>();

  378.             // set up first entries
  379.             AbsoluteDate generationDate = date;

  380.             generateCalls.incrementAndGet();
  381.             for (final T entry : generateAndCheck(null, generationDate)) {
  382.                 cache.add(new Entry(entry, quantum(entry.getDate())));
  383.             }
  384.             earliestQuantum = new AtomicLong(cache.get(0).getQuantum());
  385.             latestQuantum   = new AtomicLong(cache.get(cache.size() - 1).getQuantum());

  386.             while (cache.size() < maxNeighborsSize) {
  387.                 // we need to generate more entries

  388.                 final AbsoluteDate entry0 = cache.get(0).getData().getDate();
  389.                 final AbsoluteDate entryN = cache.get(cache.size() - 1).getData().getDate();
  390.                 generateCalls.incrementAndGet();

  391.                 final AbsoluteDate existingDate;
  392.                 if (entryN.getDate().durationFrom(date) <= date.durationFrom(entry0.getDate())) {
  393.                     // generate additional point at the end of the slot
  394.                     existingDate = entryN;
  395.                     generationDate = entryN.getDate().shiftedBy(getMeanStep() * (maxNeighborsSize - cache.size()));
  396.                     appendAtEnd(generateAndCheck(existingDate, generationDate), date);
  397.                 } else {
  398.                     // generate additional point at the start of the slot
  399.                     existingDate = entry0;
  400.                     generationDate = entry0.getDate().shiftedBy(-getMeanStep() * (maxNeighborsSize - cache.size()));
  401.                     insertAtStart(generateAndCheck(existingDate, generationDate), date);
  402.                 }

  403.             }

  404.             guessedIndex    = new AtomicInteger(cache.size() / 2);
  405.             lastAccess      = new AtomicLong(System.currentTimeMillis());

  406.         }

  407.         /** Get the earliest entry contained in the slot.
  408.          * @return earliest entry contained in the slot
  409.          */
  410.         public T getEarliest() {
  411.             return cache.get(0).getData();
  412.         }

  413.         /** Get the quantum of the earliest date contained in the slot.
  414.          * @return quantum of the earliest date contained in the slot
  415.          */
  416.         public long getEarliestQuantum() {
  417.             return earliestQuantum.get();
  418.         }

  419.         /** Get the latest entry contained in the slot.
  420.          * @return latest entry contained in the slot
  421.          */
  422.         public T getLatest() {
  423.             return cache.get(cache.size() - 1).getData();
  424.         }

  425.         /** Get the quantum of the latest date contained in the slot.
  426.          * @return quantum of the latest date contained in the slot
  427.          */
  428.         public long getLatestQuantum() {
  429.             return latestQuantum.get();
  430.         }

  431.         /** Get the number of entries contained in the slot.
  432.          * @return number of entries contained in the slot
  433.          */
  434.         public int getEntries() {
  435.             return cache.size();
  436.         }

  437.         /** Get the mean step between entries.
  438.          * <p>
  439.          * If an overriding mean step has been defined at construction, then it will be returned instead.
  440.          * @return mean step between entries (or an arbitrary non-null value
  441.          * if there are fewer than 2 entries)
  442.          */
  443.         private double getMeanStep() {
  444.             if (cache.size() < 2) {
  445.                 return 1.0;
  446.             } else {
  447.                 if (!Double.isNaN(overridingMeanStep)) {
  448.                     return overridingMeanStep;
  449.                 } else {
  450.                     final AbsoluteDate t0 = cache.get(0).getData().getDate();
  451.                     final AbsoluteDate tn = cache.get(cache.size() - 1).getData().getDate();
  452.                     return tn.durationFrom(t0) / (cache.size() - 1);
  453.                 }
  454.             }
  455.         }

  456.         /** Get last access time of slot.
  457.          * @return last known access time
  458.          */
  459.         public long getLastAccess() {
  460.             return lastAccess.get();
  461.         }

  462.         /** Get the entries surrounding a central date.
  463.          * <p>
  464.          * If the central date is well within covered slot, the returned array
  465.          * will be balanced with half the points before central date and half the
  466.          * points after it (depending on n parity, of course). If the central date
  467.          * is near slot boundary and the underlying {@link TimeStampedGenerator
  468.          * generator} cannot extend it (i.e. it returns null), then the returned
  469.          * array will be unbalanced and will contain only the n earliest (or latest)
  470.          * cached entries. A typical example of the later case is leap seconds cache,
  471.          * since the number of leap seconds cannot be arbitrarily increased.
  472.          * </p>
  473.          * @param central central date
  474.          * @param dateQuantum global quantum of the date
  475.          * @param n number of neighbors
  476.          * @return a new array containing date neighbors
  477.          */
  478.         public Stream<T> getNeighbors(final AbsoluteDate central, final long dateQuantum, final int n) {
  479.             int index         = entryIndex(central, dateQuantum);
  480.             int firstNeighbor = index - (n - 1) / 2;

  481.             if (firstNeighbor < 0 || firstNeighbor + n > cache.size()) {
  482.                 // the cache is not balanced around the desired date, we can try to generate new data

  483.                 // upgrade the read lock to a write lock so we can change the list of available slots
  484.                 lock.readLock().unlock();
  485.                 lock.writeLock().lock();

  486.                 try {
  487.                     // check entries again as another thread may have changed
  488.                     // the list while we were waiting for the write lock
  489.                     boolean loop = true;
  490.                     while (loop) {
  491.                         index         = entryIndex(central, dateQuantum);
  492.                         firstNeighbor = index - (n - 1) / 2;
  493.                         if (firstNeighbor < 0 || firstNeighbor + n > cache.size()) {

  494.                             // estimate which data we need to be generated
  495.                             final double step = getMeanStep();
  496.                             final AbsoluteDate existingDate;
  497.                             final AbsoluteDate generationDate;
  498.                             final boolean simplyRebalance;
  499.                             if (firstNeighbor < 0) {
  500.                                 existingDate    = cache.get(0).getData().getDate();
  501.                                 generationDate  = existingDate.getDate().shiftedBy(step * firstNeighbor);
  502.                                 simplyRebalance = existingDate.getDate().compareTo(central) <= 0;
  503.                             } else {
  504.                                 existingDate    = cache.get(cache.size() - 1).getData().getDate();
  505.                                 generationDate  = existingDate.getDate().shiftedBy(step * (firstNeighbor + n - cache.size()));
  506.                                 simplyRebalance = existingDate.getDate().compareTo(central) >= 0;
  507.                             }
  508.                             generateCalls.incrementAndGet();

  509.                             // generated data and add it to the slot
  510.                             try {
  511.                                 if (firstNeighbor < 0) {
  512.                                     insertAtStart(generateAndCheck(existingDate, generationDate), central);
  513.                                 } else {
  514.                                     appendAtEnd(generateAndCheck(existingDate, generationDate), central);
  515.                                 }
  516.                             } catch (TimeStampedCacheException tce) {
  517.                                 if (simplyRebalance) {
  518.                                     // we were simply trying to rebalance an unbalanced interval near slot end
  519.                                     // we failed, but the central date is already covered by the existing (unbalanced) data
  520.                                     // so we ignore the exception and stop the loop, we will continue with what we have
  521.                                     loop = false;
  522.                                 } else {
  523.                                     throw tce;
  524.                                 }
  525.                             }

  526.                         } else {
  527.                             loop = false;
  528.                         }
  529.                     }
  530.                 } finally {
  531.                     // downgrade back to a read lock
  532.                     lock.readLock().lock();
  533.                     lock.writeLock().unlock();
  534.                 }

  535.             }

  536.             if (firstNeighbor + n > cache.size()) {
  537.                 // we end up with a non-balanced neighborhood,
  538.                 // adjust the start point to fit within the cache
  539.                 firstNeighbor = cache.size() - n;
  540.             }
  541.             if (firstNeighbor < 0) {
  542.                 firstNeighbor = 0;
  543.             }
  544.             final Stream.Builder<T> builder = Stream.builder();
  545.             for (int i = 0; i < n; ++i) {
  546.                 builder.accept(cache.get(firstNeighbor + i).getData());
  547.             }

  548.             return builder.build();

  549.         }

  550.         /** Get the index of the entry corresponding to a date.
  551.          * <p>
  552.          * We own a local read lock while calling this method.
  553.          * </p>
  554.          * @param date date
  555.          * @param dateQuantum global quantum of the date
  556.          * @return index in the array such that entry[index] is before
  557.          * date and entry[index + 1] is after date (or they are at array boundaries)
  558.          */
  559.         private int entryIndex(final AbsoluteDate date, final long dateQuantum) {

  560.             // first quick guesses, assuming a recent search was close enough
  561.             final int guess = guessedIndex.get();
  562.             if (guess > 0 && guess < cache.size()) {
  563.                 if (cache.get(guess).getQuantum() <= dateQuantum) {
  564.                     if (guess + 1 < cache.size() && cache.get(guess + 1).getQuantum() > dateQuantum) {
  565.                         // good guess!
  566.                         return guess;
  567.                     } else {
  568.                         // perhaps we have simply shifted just one point forward ?
  569.                         if (guess + 2 < cache.size() && cache.get(guess + 2).getQuantum() > dateQuantum) {
  570.                             guessedIndex.set(guess + 1);
  571.                             return guess + 1;
  572.                         }
  573.                     }
  574.                 } else {
  575.                     // perhaps we have simply shifted just one point backward ?
  576.                     if (guess > 1 && cache.get(guess - 1).getQuantum() <= dateQuantum) {
  577.                         guessedIndex.set(guess - 1);
  578.                         return guess - 1;
  579.                     }
  580.                 }
  581.             }

  582.             // quick guesses have failed, we need to perform a full blown search
  583.             if (dateQuantum < getEarliestQuantum()) {
  584.                 // date if before the first entry
  585.                 return -1;
  586.             } else if (dateQuantum > getLatestQuantum()) {
  587.                 // date is after the last entry
  588.                 return cache.size();
  589.             } else {

  590.                 // try to get an existing entry
  591.                 int  iInf = 0;
  592.                 final long qInf = cache.get(iInf).getQuantum();
  593.                 int  iSup = cache.size() - 1;
  594.                 final long qSup = cache.get(iSup).getQuantum();
  595.                 while (iSup - iInf > 0) {
  596.                     // within a continuous slot, entries are expected to be roughly linear
  597.                     final int iInterp = (int) ((iInf * (qSup - dateQuantum) + iSup * (dateQuantum - qInf)) / (qSup - qInf));
  598.                     final int iMed    = FastMath.max(iInf + 1, FastMath.min(iInterp, iSup));
  599.                     final Entry entry = cache.get(iMed);
  600.                     if (dateQuantum < entry.getQuantum()) {
  601.                         iSup = iMed - 1;
  602.                     } else if (dateQuantum > entry.getQuantum()) {
  603.                         iInf = iMed;
  604.                     } else {
  605.                         guessedIndex.set(iMed);
  606.                         return iMed;
  607.                     }
  608.                 }

  609.                 guessedIndex.set(iInf);
  610.                 return iInf;

  611.             }

  612.         }

  613.         /** Insert data at slot start.
  614.          * @param data data to insert
  615.          * @param requestedDate use for the error message.
  616.          */
  617.         private void insertAtStart(final List<T> data, final AbsoluteDate requestedDate) {

  618.             // insert data at start
  619.             boolean inserted = false;
  620.             final long q0 = earliestQuantum.get();
  621.             for (int i = 0; i < data.size(); ++i) {
  622.                 final long quantum = quantum(data.get(i).getDate());
  623.                 if (quantum < q0) {
  624.                     cache.add(i, new Entry(data.get(i), quantum));
  625.                     inserted = true;
  626.                 } else {
  627.                     break;
  628.                 }
  629.             }

  630.             if (!inserted) {
  631.                 final AbsoluteDate earliest = cache.get(0).getData().getDate();
  632.                 throw new TimeStampedCacheException(
  633.                         OrekitMessages.UNABLE_TO_GENERATE_NEW_DATA_BEFORE,
  634.                         earliest, requestedDate, earliest.durationFrom(requestedDate));
  635.             }

  636.             // evict excess data at end
  637.             final AbsoluteDate t0 = cache.get(0).getData().getDate();
  638.             while (cache.size() > maxNeighborsSize &&
  639.                    cache.get(cache.size() - 1).getData().getDate().durationFrom(t0) > maxSpan) {
  640.                 cache.remove(cache.size() - 1);
  641.             }

  642.             // update boundaries
  643.             earliestQuantum.set(cache.get(0).getQuantum());
  644.             latestQuantum.set(cache.get(cache.size() - 1).getQuantum());

  645.         }

  646.         /** Append data at slot end.
  647.          * @param data data to append
  648.          * @param requestedDate use for error message.
  649.          */
  650.         private void appendAtEnd(final List<T> data, final AbsoluteDate requestedDate) {

  651.             // append data at end
  652.             boolean appended = false;
  653.             final long qn = latestQuantum.get();
  654.             final int  n  = cache.size();
  655.             for (int i = data.size() - 1; i >= 0; --i) {
  656.                 final long quantum = quantum(data.get(i).getDate());
  657.                 if (quantum > qn) {
  658.                     cache.add(n, new Entry(data.get(i), quantum));
  659.                     appended = true;
  660.                 } else {
  661.                     break;
  662.                 }
  663.             }

  664.             if (!appended) {
  665.                 final AbsoluteDate latest = cache.get(cache.size() - 1).getData().getDate();
  666.                 throw new TimeStampedCacheException(
  667.                         OrekitMessages.UNABLE_TO_GENERATE_NEW_DATA_AFTER,
  668.                         latest, requestedDate, requestedDate.durationFrom(latest));
  669.             }

  670.             // evict excess data at start
  671.             final AbsoluteDate tn = cache.get(cache.size() - 1).getData().getDate();
  672.             while (cache.size() > maxNeighborsSize &&
  673.                    tn.durationFrom(cache.get(0).getData().getDate()) > maxSpan) {
  674.                 cache.remove(0);
  675.             }

  676.             // update boundaries
  677.             earliestQuantum.set(cache.get(0).getQuantum());
  678.             latestQuantum.set(cache.get(cache.size() - 1).getQuantum());

  679.         }

  680.         /** Generate entries and check ordering.
  681.          * @param existingDate date of the closest already existing entry (may be null)
  682.          * @param date date that must be covered by the range of the generated array
  683.          * (guaranteed to lie between {@link #getEarliest()} and {@link #getLatest()})
  684.          * @return chronologically sorted list of generated entries
  685.          */
  686.         private List<T> generateAndCheck(final AbsoluteDate existingDate, final AbsoluteDate date) {
  687.             final List<T> entries = generator.generate(existingDate, date);
  688.             if (entries.isEmpty()) {
  689.                 throw new TimeStampedCacheException(OrekitMessages.NO_DATA_GENERATED, date);
  690.             }
  691.             for (int i = 1; i < entries.size(); ++i) {
  692.                 final AbsoluteDate previous = entries.get(i - 1).getDate();
  693.                 final AbsoluteDate current = entries.get(i).getDate();
  694.                 if (current.compareTo(previous) < 0) {
  695.                     throw new TimeStampedCacheException(OrekitMessages.NON_CHRONOLOGICALLY_SORTED_ENTRIES,
  696.                             previous, current, previous.durationFrom(current));
  697.                 }
  698.             }
  699.             return entries;
  700.         }

  701.         /** Container for entries. */
  702.         private class Entry {

  703.             /** Entry data. */
  704.             private final T data;

  705.             /** Global quantum of the entry. */
  706.             private final long quantum;

  707.             /** Simple constructor.
  708.              * @param data entry data
  709.              * @param quantum entry quantum
  710.              */
  711.             Entry(final T data, final long quantum) {
  712.                 this.quantum = quantum;
  713.                 this.data  = data;
  714.             }

  715.             /** Get the quantum.
  716.              * @return quantum
  717.              */
  718.             public long getQuantum() {
  719.                 return quantum;
  720.             }

  721.             /** Get the data.
  722.              * @return data
  723.              */
  724.             public T getData() {
  725.                 return data;
  726.             }

  727.         }
  728.     }

  729. }