GenericTimeStampedCache.java

  1. /* Copyright 2002-2022 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.OrekitIllegalArgumentException;
  29. import org.orekit.errors.OrekitIllegalStateException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.errors.TimeStampedCacheException;
  32. import org.orekit.time.AbsoluteDate;
  33. import org.orekit.time.TimeStamped;

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

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

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

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

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

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

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

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

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

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

  53.     /** Number of entries in a neighbors array. */
  54.     private final int neighborsSize;

  55.     /** Independent time slots cached. */
  56.     private final List<Slot> slots;

  57.     /** Number of calls to the getNeighbors method. */
  58.     private final AtomicInteger getNeighborsCalls;

  59.     /** Number of calls to the generate method. */
  60.     private final AtomicInteger generateCalls;

  61.     /** Number of evictions. */
  62.     private final AtomicInteger evictions;

  63.     /** Global lock. */
  64.     private final ReadWriteLock lock;

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

  77.         // safety check
  78.         if (maxSlots < 1) {
  79.             throw new OrekitIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL, maxSlots, 1);
  80.         }
  81.         if (neighborsSize < 2) {
  82.             throw new OrekitIllegalArgumentException(OrekitMessages.NOT_ENOUGH_CACHED_NEIGHBORS,
  83.                                                      neighborsSize, 2);
  84.         }

  85.         this.reference         = new AtomicReference<AbsoluteDate>();
  86.         this.maxSlots          = maxSlots;
  87.         this.maxSpan           = maxSpan;
  88.         this.newSlotQuantumGap = FastMath.round(newSlotInterval / QUANTUM_STEP);
  89.         this.generator         = generator;
  90.         this.neighborsSize     = neighborsSize;
  91.         this.slots             = new ArrayList<Slot>(maxSlots);
  92.         this.getNeighborsCalls = new AtomicInteger(0);
  93.         this.generateCalls     = new AtomicInteger(0);
  94.         this.evictions         = new AtomicInteger(0);
  95.         this.lock              = new ReentrantReadWriteLock();

  96.     }

  97.     /** Get the generator.
  98.      * @return generator
  99.      */
  100.     public TimeStampedGenerator<T> getGenerator() {
  101.         return generator;
  102.     }

  103.     /** Get the maximum number of independent cached time slots.
  104.      * @return maximum number of independent cached time slots
  105.      */
  106.     public int getMaxSlots() {
  107.         return maxSlots;
  108.     }

  109.     /** Get the maximum duration span in seconds of one slot.
  110.      * @return maximum duration span in seconds of one slot
  111.      */
  112.     public double getMaxSpan() {
  113.         return maxSpan;
  114.     }

  115.     /** Get quantum gap above which a new slot is created instead of extending an existing one.
  116.      * <p>
  117.      * The quantum gap is the {@code newSlotInterval} value provided at construction
  118.      * rounded to the nearest quantum step used internally by the cache.
  119.      * </p>
  120.      * @return quantum gap in seconds
  121.      */
  122.     public double getNewSlotQuantumGap() {
  123.         return newSlotQuantumGap * QUANTUM_STEP;
  124.     }

  125.     /** Get the number of calls to the {@link #getNeighbors(AbsoluteDate)} method.
  126.      * <p>
  127.      * This number of calls is used as a reference to interpret {@link #getGenerateCalls()}.
  128.      * </p>
  129.      * @return number of calls to the {@link #getNeighbors(AbsoluteDate)} method
  130.      * @see #getGenerateCalls()
  131.      */
  132.     public int getGetNeighborsCalls() {
  133.         return getNeighborsCalls.get();
  134.     }

  135.     /** Get the number of calls to the generate method.
  136.      * <p>
  137.      * This number of calls is related to the number of cache misses and may
  138.      * be used to tune the cache configuration. Each cache miss implies at
  139.      * least one call is performed, but may require several calls if the new
  140.      * date is far offset from the existing cache, depending on the number of
  141.      * elements and step between elements in the arrays returned by the generator.
  142.      * </p>
  143.      * @return number of calls to the generate method
  144.      * @see #getGetNeighborsCalls()
  145.      */
  146.     public int getGenerateCalls() {
  147.         return generateCalls.get();
  148.     }

  149.     /** Get the number of slots evictions.
  150.      * <p>
  151.      * This number should remain small when the max number of slots is sufficient
  152.      * with respect to the number of concurrent requests to the cache. If it
  153.      * increases too much, then the cache configuration is probably bad and cache
  154.      * does not really improve things (in this case, the {@link #getGenerateCalls()
  155.      * number of calls to the generate method} will probably increase too.
  156.      * </p>
  157.      * @return number of slots evictions
  158.      */
  159.     public int getSlotsEvictions() {
  160.         return evictions.get();
  161.     }

  162.     /** Get the number of slots in use.
  163.      * @return number of slots in use
  164.      */
  165.     public int getSlots() {

  166.         lock.readLock().lock();
  167.         try {
  168.             return slots.size();
  169.         } finally {
  170.             lock.readLock().unlock();
  171.         }

  172.     }

  173.     /** Get the total number of entries cached.
  174.      * @return total number of entries cached
  175.      */
  176.     public int getEntries() {

  177.         lock.readLock().lock();
  178.         try {
  179.             int entries = 0;
  180.             for (final Slot slot : slots) {
  181.                 entries += slot.getEntries();
  182.             }
  183.             return entries;
  184.         } finally {
  185.             lock.readLock().unlock();
  186.         }

  187.     }

  188.     /** Get the earliest cached entry.
  189.      * @return earliest cached entry
  190.      * @exception IllegalStateException if the cache has no slots at all
  191.      * @see #getSlots()
  192.      */
  193.     public T getEarliest() throws IllegalStateException {

  194.         lock.readLock().lock();
  195.         try {
  196.             if (slots.isEmpty()) {
  197.                 throw new OrekitIllegalStateException(OrekitMessages.NO_CACHED_ENTRIES);
  198.             }
  199.             return slots.get(0).getEarliest();
  200.         } finally {
  201.             lock.readLock().unlock();
  202.         }

  203.     }

  204.     /** Get the latest cached entry.
  205.      * @return latest cached entry
  206.      * @exception IllegalStateException if the cache has no slots at all
  207.      * @see #getSlots()
  208.      */
  209.     public T getLatest() throws IllegalStateException {

  210.         lock.readLock().lock();
  211.         try {
  212.             if (slots.isEmpty()) {
  213.                 throw new OrekitIllegalStateException(OrekitMessages.NO_CACHED_ENTRIES);
  214.             }
  215.             return slots.get(slots.size() - 1).getLatest();
  216.         } finally {
  217.             lock.readLock().unlock();
  218.         }

  219.     }

  220.     /** Get the fixed size of the arrays to be returned by {@link #getNeighbors(AbsoluteDate)}.
  221.      * @return size of the array
  222.      */
  223.     public int getNeighborsSize() {
  224.         return neighborsSize;
  225.     }

  226.     /** Get the entries surrounding a central date.
  227.      * <p>
  228.      * If the central date is well within covered range, the returned array
  229.      * will be balanced with half the points before central date and half the
  230.      * points after it (depending on n parity, of course). If the central date
  231.      * is near the generator range boundary, then the returned array will be
  232.      * unbalanced and will contain only the n earliest (or latest) generated
  233.      * (and cached) entries. A typical example of the later case is leap seconds
  234.      * cache, since the number of leap seconds cannot be arbitrarily increased.
  235.      * </p>
  236.      * @param central central date
  237.      * @return array of cached entries surrounding specified date (the size
  238.      * of the array is fixed to the one specified in the {@link
  239.      * #GenericTimeStampedCache(int, int, double, double, TimeStampedGenerator)}
  240.      * @see #getEarliest()
  241.      * @see #getLatest()
  242.      */
  243.     public Stream<T> getNeighbors(final AbsoluteDate central) {

  244.         lock.readLock().lock();
  245.         try {
  246.             getNeighborsCalls.incrementAndGet();
  247.             final long dateQuantum = quantum(central);
  248.             return selectSlot(central, dateQuantum).getNeighbors(central, dateQuantum);
  249.         } finally {
  250.             lock.readLock().unlock();
  251.         }

  252.     }

  253.     /** Convert a date to a rough global quantum.
  254.      * <p>
  255.      * We own a global read lock while calling this method.
  256.      * </p>
  257.      * @param date date to convert
  258.      * @return quantum corresponding to the date
  259.      */
  260.     private long quantum(final AbsoluteDate date) {
  261.         reference.compareAndSet(null, date);
  262.         return FastMath.round(date.durationFrom(reference.get()) / QUANTUM_STEP);
  263.     }

  264.     /** Select a slot containing a date.
  265.      * <p>
  266.      * We own a global read lock while calling this method.
  267.      * </p>
  268.      * @param date target date
  269.      * @param dateQuantum global quantum of the date
  270.      * @return slot covering the date
  271.      */
  272.     private Slot selectSlot(final AbsoluteDate date, final long dateQuantum) {

  273.         Slot selected = null;

  274.         int index = slots.isEmpty() ? 0 : slotIndex(dateQuantum);
  275.         if (slots.isEmpty() ||
  276.             slots.get(index).getEarliestQuantum() > dateQuantum + newSlotQuantumGap ||
  277.             slots.get(index).getLatestQuantum()   < dateQuantum - newSlotQuantumGap) {
  278.             // no existing slot is suitable

  279.             // upgrade the read lock to a write lock so we can change the list of available slots
  280.             lock.readLock().unlock();
  281.             lock.writeLock().lock();

  282.             try {
  283.                 // check slots again as another thread may have changed
  284.                 // the list while we were waiting for the write lock
  285.                 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.                     // we really need to create a new slot in the current thread
  290.                     // (no other threads have created it while we were waiting for the lock)
  291.                     if (!slots.isEmpty() &&
  292.                         slots.get(index).getLatestQuantum() < dateQuantum - newSlotQuantumGap) {
  293.                         ++index;
  294.                     }

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

  297.                         // select the oldest accessed slot for eviction
  298.                         int evict = 0;
  299.                         for (int i = 0; i < slots.size(); ++i) {
  300.                             if (slots.get(i).getLastAccess() < slots.get(evict).getLastAccess()) {
  301.                                 evict = i;
  302.                             }
  303.                         }

  304.                         // evict the selected slot
  305.                         evictions.incrementAndGet();
  306.                         slots.remove(evict);

  307.                         if (evict < index) {
  308.                             // adjust index of created slot as it was shifted by the eviction
  309.                             index--;
  310.                         }
  311.                     }

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

  313.                 }

  314.             } finally {
  315.                 // downgrade back to a read lock
  316.                 lock.readLock().lock();
  317.                 lock.writeLock().unlock();
  318.             }
  319.         }

  320.         selected = slots.get(index);


  321.         return selected;

  322.     }

  323.     /** Get the index of the slot in which a date could be cached.
  324.      * <p>
  325.      * We own a global read lock while calling this method.
  326.      * </p>
  327.      * @param dateQuantum quantum of the date to search for
  328.      * @return the slot in which the date could be cached
  329.      */
  330.     private int slotIndex(final long dateQuantum) {

  331.         int  iInf = 0;
  332.         final long qInf = slots.get(iInf).getEarliestQuantum();
  333.         int  iSup = slots.size() - 1;
  334.         final long qSup = slots.get(iSup).getLatestQuantum();
  335.         while (iSup - iInf > 0) {
  336.             final int iInterp = (int) ((iInf * (qSup - dateQuantum) + iSup * (dateQuantum - qInf)) / (qSup - qInf));
  337.             final int iMed    = FastMath.max(iInf, FastMath.min(iInterp, iSup));
  338.             final Slot slot   = slots.get(iMed);
  339.             if (dateQuantum < slot.getEarliestQuantum()) {
  340.                 iSup = iMed - 1;
  341.             } else if (dateQuantum > slot.getLatestQuantum()) {
  342.                 iInf = FastMath.min(iSup, iMed + 1);
  343.             } else {
  344.                 return iMed;
  345.             }
  346.         }

  347.         return iInf;

  348.     }

  349.     /** Time slot. */
  350.     private final class Slot {

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

  353.         /** Earliest quantum. */
  354.         private AtomicLong earliestQuantum;

  355.         /** Latest quantum. */
  356.         private AtomicLong latestQuantum;

  357.         /** Index from a previous recent call. */
  358.         private AtomicInteger guessedIndex;

  359.         /** Last access time. */
  360.         private AtomicLong lastAccess;

  361.         /** Simple constructor.
  362.          * @param date central date for initial entries to insert in the slot
  363.          */
  364.         Slot(final AbsoluteDate date) {

  365.             // allocate cache
  366.             this.cache = new ArrayList<Entry>();

  367.             // set up first entries
  368.             AbsoluteDate generationDate = date;

  369.             generateCalls.incrementAndGet();
  370.             for (final T entry : generateAndCheck(null, generationDate)) {
  371.                 cache.add(new Entry(entry, quantum(entry.getDate())));
  372.             }
  373.             earliestQuantum = new AtomicLong(cache.get(0).getQuantum());
  374.             latestQuantum   = new AtomicLong(cache.get(cache.size() - 1).getQuantum());

  375.             while (cache.size() < neighborsSize) {
  376.                 // we need to generate more entries

  377.                 final AbsoluteDate entry0 = cache.get(0).getData().getDate();
  378.                 final AbsoluteDate entryN = cache.get(cache.size() - 1).getData().getDate();
  379.                 generateCalls.incrementAndGet();

  380.                 final AbsoluteDate existingDate;
  381.                 if (entryN.getDate().durationFrom(date) <= date.durationFrom(entry0.getDate())) {
  382.                     // generate additional point at the end of the slot
  383.                     existingDate = entryN;
  384.                     generationDate = entryN.getDate().shiftedBy(getMeanStep() * (neighborsSize - cache.size()));
  385.                     appendAtEnd(generateAndCheck(existingDate, generationDate), date);
  386.                 } else {
  387.                     // generate additional point at the start of the slot
  388.                     existingDate = entry0;
  389.                     generationDate = entry0.getDate().shiftedBy(-getMeanStep() * (neighborsSize - cache.size()));
  390.                     insertAtStart(generateAndCheck(existingDate, generationDate), date);
  391.                 }

  392.             }

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

  395.         }

  396.         /** Get the earliest entry contained in the slot.
  397.          * @return earliest entry contained in the slot
  398.          */
  399.         public T getEarliest() {
  400.             return cache.get(0).getData();
  401.         }

  402.         /** Get the quantum of the earliest date contained in the slot.
  403.          * @return quantum of the earliest date contained in the slot
  404.          */
  405.         public long getEarliestQuantum() {
  406.             return earliestQuantum.get();
  407.         }

  408.         /** Get the latest entry contained in the slot.
  409.          * @return latest entry contained in the slot
  410.          */
  411.         public T getLatest() {
  412.             return cache.get(cache.size() - 1).getData();
  413.         }

  414.         /** Get the quantum of the latest date contained in the slot.
  415.          * @return quantum of the latest date contained in the slot
  416.          */
  417.         public long getLatestQuantum() {
  418.             return latestQuantum.get();
  419.         }

  420.         /** Get the number of entries contained din the slot.
  421.          * @return number of entries contained din the slot
  422.          */
  423.         public int getEntries() {
  424.             return cache.size();
  425.         }

  426.         /** Get the mean step between entries.
  427.          * @return mean step between entries (or an arbitrary non-null value
  428.          * if there are fewer than 2 entries)
  429.          */
  430.         private double getMeanStep() {
  431.             if (cache.size() < 2) {
  432.                 return 1.0;
  433.             } else {
  434.                 final AbsoluteDate t0 = cache.get(0).getData().getDate();
  435.                 final AbsoluteDate tn = cache.get(cache.size() - 1).getData().getDate();
  436.                 return tn.durationFrom(t0) / (cache.size() - 1);
  437.             }
  438.         }

  439.         /** Get last access time of slot.
  440.          * @return last known access time
  441.          */
  442.         public long getLastAccess() {
  443.             return lastAccess.get();
  444.         }

  445.         /** Get the entries surrounding a central date.
  446.          * <p>
  447.          * If the central date is well within covered slot, the returned array
  448.          * will be balanced with half the points before central date and half the
  449.          * points after it (depending on n parity, of course). If the central date
  450.          * is near slot boundary and the underlying {@link TimeStampedGenerator
  451.          * generator} cannot extend it (i.e. it returns null), then the returned
  452.          * array will be unbalanced and will contain only the n earliest (or latest)
  453.          * cached entries. A typical example of the later case is leap seconds cache,
  454.          * since the number of leap seconds cannot be arbitrarily increased.
  455.          * </p>
  456.          * @param central central date
  457.          * @param dateQuantum global quantum of the date
  458.          * @return a new array containing date neighbors
  459.          * @see #getBefore(AbsoluteDate)
  460.          * @see #getAfter(AbsoluteDate)
  461.          */
  462.         public Stream<T> getNeighbors(final AbsoluteDate central, final long dateQuantum) {

  463.             int index         = entryIndex(central, dateQuantum);
  464.             int firstNeighbor = index - (neighborsSize - 1) / 2;

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

  467.                 // upgrade the read lock to a write lock so we can change the list of available slots
  468.                 lock.readLock().unlock();
  469.                 lock.writeLock().lock();

  470.                 try {
  471.                     // check entries again as another thread may have changed
  472.                     // the list while we were waiting for the write lock
  473.                     boolean loop = true;
  474.                     while (loop) {
  475.                         index         = entryIndex(central, dateQuantum);
  476.                         firstNeighbor = index - (neighborsSize - 1) / 2;
  477.                         if (firstNeighbor < 0 || firstNeighbor + neighborsSize > cache.size()) {

  478.                             // estimate which data we need to be generated
  479.                             final double step = getMeanStep();
  480.                             final AbsoluteDate existingDate;
  481.                             final AbsoluteDate generationDate;
  482.                             final boolean simplyRebalance;
  483.                             if (firstNeighbor < 0) {
  484.                                 existingDate    = cache.get(0).getData().getDate();
  485.                                 generationDate  = existingDate.getDate().shiftedBy(step * firstNeighbor);
  486.                                 simplyRebalance = existingDate.getDate().compareTo(central) <= 0;
  487.                             } else {
  488.                                 existingDate    = cache.get(cache.size() - 1).getData().getDate();
  489.                                 generationDate  = existingDate.getDate().shiftedBy(step * (firstNeighbor + neighborsSize - cache.size()));
  490.                                 simplyRebalance = existingDate.getDate().compareTo(central) >= 0;
  491.                             }
  492.                             generateCalls.incrementAndGet();

  493.                             // generated data and add it to the slot
  494.                             try {
  495.                                 if (firstNeighbor < 0) {
  496.                                     insertAtStart(generateAndCheck(existingDate, generationDate), central);
  497.                                 } else {
  498.                                     appendAtEnd(generateAndCheck(existingDate, generationDate), central);
  499.                                 }
  500.                             } catch (TimeStampedCacheException tce) {
  501.                                 if (simplyRebalance) {
  502.                                     // we were simply trying to rebalance an unbalanced interval near slot end
  503.                                     // we failed, but the central date is already covered by the existing (unbalanced) data
  504.                                     // so we ignore the exception and stop the loop, we will continue with what we have
  505.                                     loop = false;
  506.                                 } else {
  507.                                     throw tce;
  508.                                 }
  509.                             }

  510.                         } else {
  511.                             loop = false;
  512.                         }
  513.                     }
  514.                 } finally {
  515.                     // downgrade back to a read lock
  516.                     lock.readLock().lock();
  517.                     lock.writeLock().unlock();
  518.                 }

  519.             }

  520.             if (firstNeighbor + neighborsSize > cache.size()) {
  521.                 // we end up with a non-balanced neighborhood,
  522.                 // adjust the start point to fit within the cache
  523.                 firstNeighbor = cache.size() - neighborsSize;
  524.             }
  525.             if (firstNeighbor < 0) {
  526.                 firstNeighbor = 0;
  527.             }
  528.             final Stream.Builder<T> builder = Stream.builder();
  529.             for (int i = 0; i < neighborsSize; ++i) {
  530.                 builder.accept(cache.get(firstNeighbor + i).getData());
  531.             }

  532.             return builder.build();

  533.         }

  534.         /** Get the index of the entry corresponding to a date.
  535.          * <p>
  536.          * We own a local read lock while calling this method.
  537.          * </p>
  538.          * @param date date
  539.          * @param dateQuantum global quantum of the date
  540.          * @return index in the array such that entry[index] is before
  541.          * date and entry[index + 1] is after date (or they are at array boundaries)
  542.          */
  543.         private int entryIndex(final AbsoluteDate date, final long dateQuantum) {

  544.             // first quick guesses, assuming a recent search was close enough
  545.             final int guess = guessedIndex.get();
  546.             if (guess > 0 && guess < cache.size()) {
  547.                 if (cache.get(guess).getQuantum() <= dateQuantum) {
  548.                     if (guess + 1 < cache.size() && cache.get(guess + 1).getQuantum() > dateQuantum) {
  549.                         // good guess!
  550.                         return guess;
  551.                     } else {
  552.                         // perhaps we have simply shifted just one point forward ?
  553.                         if (guess + 2 < cache.size() && cache.get(guess + 2).getQuantum() > dateQuantum) {
  554.                             guessedIndex.set(guess + 1);
  555.                             return guess + 1;
  556.                         }
  557.                     }
  558.                 } else {
  559.                     // perhaps we have simply shifted just one point backward ?
  560.                     if (guess > 1 && cache.get(guess - 1).getQuantum() <= dateQuantum) {
  561.                         guessedIndex.set(guess - 1);
  562.                         return guess - 1;
  563.                     }
  564.                 }
  565.             }

  566.             // quick guesses have failed, we need to perform a full blown search
  567.             if (dateQuantum < getEarliestQuantum()) {
  568.                 // date if before the first entry
  569.                 return -1;
  570.             } else if (dateQuantum > getLatestQuantum()) {
  571.                 // date is after the last entry
  572.                 return cache.size();
  573.             } else {

  574.                 // try to get an existing entry
  575.                 int  iInf = 0;
  576.                 final long qInf = cache.get(iInf).getQuantum();
  577.                 int  iSup = cache.size() - 1;
  578.                 final long qSup = cache.get(iSup).getQuantum();
  579.                 while (iSup - iInf > 0) {
  580.                     // within a continuous slot, entries are expected to be roughly linear
  581.                     final int iInterp = (int) ((iInf * (qSup - dateQuantum) + iSup * (dateQuantum - qInf)) / (qSup - qInf));
  582.                     final int iMed    = FastMath.max(iInf + 1, FastMath.min(iInterp, iSup));
  583.                     final Entry entry = cache.get(iMed);
  584.                     if (dateQuantum < entry.getQuantum()) {
  585.                         iSup = iMed - 1;
  586.                     } else if (dateQuantum > entry.getQuantum()) {
  587.                         iInf = iMed;
  588.                     } else {
  589.                         guessedIndex.set(iMed);
  590.                         return iMed;
  591.                     }
  592.                 }

  593.                 guessedIndex.set(iInf);
  594.                 return iInf;

  595.             }

  596.         }

  597.         /** Insert data at slot start.
  598.          * @param data data to insert
  599.          * @param requestedDate use for the error message.
  600.          */
  601.         private void insertAtStart(final List<T> data, final AbsoluteDate requestedDate) {

  602.             // insert data at start
  603.             boolean inserted = false;
  604.             final long q0 = earliestQuantum.get();
  605.             for (int i = 0; i < data.size(); ++i) {
  606.                 final long quantum = quantum(data.get(i).getDate());
  607.                 if (quantum < q0) {
  608.                     cache.add(i, new Entry(data.get(i), quantum));
  609.                     inserted = true;
  610.                 } else {
  611.                     break;
  612.                 }
  613.             }

  614.             if (!inserted) {
  615.                 final AbsoluteDate earliest = cache.get(0).getData().getDate();
  616.                 throw new TimeStampedCacheException(
  617.                         OrekitMessages.UNABLE_TO_GENERATE_NEW_DATA_BEFORE,
  618.                         earliest, requestedDate, earliest.durationFrom(requestedDate));
  619.             }

  620.             // evict excess data at end
  621.             final AbsoluteDate t0 = cache.get(0).getData().getDate();
  622.             while (cache.size() > neighborsSize &&
  623.                    cache.get(cache.size() - 1).getData().getDate().durationFrom(t0) > maxSpan) {
  624.                 cache.remove(cache.size() - 1);
  625.             }

  626.             // update boundaries
  627.             earliestQuantum.set(cache.get(0).getQuantum());
  628.             latestQuantum.set(cache.get(cache.size() - 1).getQuantum());

  629.         }

  630.         /** Append data at slot end.
  631.          * @param data data to append
  632.          * @param requestedDate use for error message.
  633.          */
  634.         private void appendAtEnd(final List<T> data, final AbsoluteDate requestedDate) {

  635.             // append data at end
  636.             boolean appended = false;
  637.             final long qn = latestQuantum.get();
  638.             final int  n  = cache.size();
  639.             for (int i = data.size() - 1; i >= 0; --i) {
  640.                 final long quantum = quantum(data.get(i).getDate());
  641.                 if (quantum > qn) {
  642.                     cache.add(n, new Entry(data.get(i), quantum));
  643.                     appended = true;
  644.                 } else {
  645.                     break;
  646.                 }
  647.             }

  648.             if (!appended) {
  649.                 final AbsoluteDate latest = cache.get(cache.size() - 1).getData().getDate();
  650.                 throw new TimeStampedCacheException(
  651.                         OrekitMessages.UNABLE_TO_GENERATE_NEW_DATA_AFTER,
  652.                         latest, requestedDate, requestedDate.durationFrom(latest));
  653.             }

  654.             // evict excess data at start
  655.             final AbsoluteDate tn = cache.get(cache.size() - 1).getData().getDate();
  656.             while (cache.size() > neighborsSize &&
  657.                    tn.durationFrom(cache.get(0).getData().getDate()) > maxSpan) {
  658.                 cache.remove(0);
  659.             }

  660.             // update boundaries
  661.             earliestQuantum.set(cache.get(0).getQuantum());
  662.             latestQuantum.set(cache.get(cache.size() - 1).getQuantum());

  663.         }

  664.         /** Generate entries and check ordering.
  665.          * @param existingDate date of the closest already existing entry (may be null)
  666.          * @param date date that must be covered by the range of the generated array
  667.          * (guaranteed to lie between {@link #getEarliest()} and {@link #getLatest()})
  668.          * @return chronologically sorted list of generated entries
  669.          */
  670.         private List<T> generateAndCheck(final AbsoluteDate existingDate, final AbsoluteDate date) {
  671.             final List<T> entries = generator.generate(existingDate, date);
  672.             if (entries.isEmpty()) {
  673.                 throw new TimeStampedCacheException(OrekitMessages.NO_DATA_GENERATED, date);
  674.             }
  675.             for (int i = 1; i < entries.size(); ++i) {
  676.                 final AbsoluteDate previous = entries.get(i - 1).getDate();
  677.                 final AbsoluteDate current = entries.get(i).getDate();
  678.                 if (current.compareTo(previous) < 0) {
  679.                     throw new TimeStampedCacheException(OrekitMessages.NON_CHRONOLOGICALLY_SORTED_ENTRIES,
  680.                             previous, current, previous.durationFrom(current));
  681.                 }
  682.             }
  683.             return entries;
  684.         }

  685.         /** Container for entries. */
  686.         private class Entry {

  687.             /** Entry data. */
  688.             private final T data;

  689.             /** Global quantum of the entry. */
  690.             private final long quantum;

  691.             /** Simple constructor.
  692.              * @param data entry data
  693.              * @param quantum entry quantum
  694.              */
  695.             Entry(final T data, final long quantum) {
  696.                 this.quantum = quantum;
  697.                 this.data  = data;
  698.             }

  699.             /** Get the quantum.
  700.              * @return quantum
  701.              */
  702.             public long getQuantum() {
  703.                 return quantum;
  704.             }

  705.             /** Get the data.
  706.              * @return data
  707.              */
  708.             public T getData() {
  709.                 return data;
  710.             }

  711.         }
  712.     }

  713. }