From 1b4fc6fb09c6d6fa48eb18a8326f7e4d8a9432a0 Mon Sep 17 00:00:00 2001
From: Luc Maisonobe <luc@orekit.org>
Date: Sat, 8 Mar 2014 20:08:13 +0100
Subject: [PATCH] Implemented TilesCache.

---
 .../orekit/rugged/core/dem/TileFactory.java   |   5 +-
 .../orekit/rugged/core/dem/TilesCache.java    | 336 ++++++++++++++++++
 .../core/dem/ConstantElevationUpdater.java    |  46 +++
 .../rugged/core/dem/CountingFactory.java      |  39 ++
 .../rugged/core/dem/TilesCacheTest.java       | 151 ++++++++
 5 files changed, 575 insertions(+), 2 deletions(-)
 create mode 100644 rugged-core/src/main/java/org/orekit/rugged/core/dem/TilesCache.java
 create mode 100644 rugged-core/src/test/java/orekit/rugged/core/dem/ConstantElevationUpdater.java
 create mode 100644 rugged-core/src/test/java/orekit/rugged/core/dem/CountingFactory.java
 create mode 100644 rugged-core/src/test/java/orekit/rugged/core/dem/TilesCacheTest.java

diff --git a/rugged-core/src/main/java/org/orekit/rugged/core/dem/TileFactory.java b/rugged-core/src/main/java/org/orekit/rugged/core/dem/TileFactory.java
index 8a99d15a..14425da6 100644
--- a/rugged-core/src/main/java/org/orekit/rugged/core/dem/TileFactory.java
+++ b/rugged-core/src/main/java/org/orekit/rugged/core/dem/TileFactory.java
@@ -18,12 +18,13 @@ package org.orekit.rugged.core.dem;
 
 
 /** Interface representing a factory for raster tile.
+ * @param <T> Type of tiles.
  * @author Luc Maisonobe
  */
-public interface TileFactory {
+public interface TileFactory<T extends Tile> {
 
     /** Create an empty tile.
      */
-    Tile createTile();
+    T createTile();
 
 }
diff --git a/rugged-core/src/main/java/org/orekit/rugged/core/dem/TilesCache.java b/rugged-core/src/main/java/org/orekit/rugged/core/dem/TilesCache.java
new file mode 100644
index 00000000..c8c5c1cb
--- /dev/null
+++ b/rugged-core/src/main/java/org/orekit/rugged/core/dem/TilesCache.java
@@ -0,0 +1,336 @@
+/* Copyright 2013-2014 CS Systèmes d'Information
+ * Licensed to CS Systèmes d'Information (CS) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * CS licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.orekit.rugged.core.dem;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import org.orekit.rugged.api.TileUpdater;
+
+/** Cache for Digital Elevation Model {@link Tile tiles}.
+ * <p>
+ * Beware, this cache is <em>not</em> thread-safe!
+ * </p>
+ * @param <T> Type of tiles.
+ * @author Luc Maisonobe
+ */
+public class TilesCache<T extends Tile> {
+
+    /** Factory for empty tiles. */
+    private final TileFactory<T> factory;
+
+    /** Updater for retrieving tiles data. */
+    private final TileUpdater updater;
+
+    /** Search optimized cache. */
+    private List<TilesStrip> searchCache;
+
+    /** Eviction optimized cache. */
+    private T[] evictionCache;
+
+    /** Index for next tile. */
+    private int next;
+
+    /** Simple constructor.
+     * @param factory factory for creating empty tiles
+     * @param updater updater for retrieving tiles data
+     * @param maxTiles maximum number of tiles stored simultaneously in the cache
+     */
+    public TilesCache(final TileFactory<T> factory, final TileUpdater updater, final int maxTiles) {
+        this.factory       = factory;
+        this.updater       = updater;
+        this.searchCache   = new ArrayList<TilesStrip>();
+        @SuppressWarnings("unchecked")
+        final T[] array    = (T[]) Array.newInstance(Tile.class, maxTiles);
+        this.evictionCache = array;
+        this.next          = 0;
+    }
+
+    /** Get the tile covering a ground point.
+     * @param latitude ground point latitude
+     * @param longitude ground point longitude
+     * @return tile covering the ground point
+     */
+    public T getTile(final double latitude, final double longitude) {
+        return getStrip(latitude, longitude).getTile(latitude, longitude);
+    }
+
+    /** Create a tile covering a ground point.
+     * @param latitude latitude of the point
+     * @param longitude longitude of the point
+     * @return new tile covering the point
+     */
+    private T createTile(final double latitude, final double longitude) {
+
+        if (evictionCache[next] != null) {
+            // the tile we are creating will evict this older tile
+            // we need to remove it from the search cache too
+            for (final Iterator<TilesStrip> iterator = searchCache.iterator(); iterator.hasNext();) {
+                if (iterator.next().removeTile(evictionCache[next])) {
+                    break;
+                }
+            }
+        }
+
+        // create the tile and retrieve its data
+        final T tile = factory.createTile();
+        updater.updateTile(latitude, longitude, tile);
+
+        // store the tile in the eviction cache
+        evictionCache[next] = tile;
+        next = (next + 1) % evictionCache.length;
+
+        return tile;
+
+    }
+
+    /** Get a strip covering a ground point.
+     * @param latitude ground point latitude
+     * @param longitude ground point longitude
+     * @return strip covering the ground point
+     */
+    private TilesStrip getStrip(final double latitude, final double longitude) {
+
+        // look for a strip at the specified latitude
+        final int index = Collections.binarySearch(searchCache, new BasicLatitudeProvider(latitude),
+                                                   new LatitudeComparator());
+        if (index >= 0) {
+            // rare case, the latitude is an exact maximum latitude for a strip
+            return searchCache.get(index);
+        } else {
+
+            final int insertionPoint  = -(index + 1);
+
+            if (insertionPoint < searchCache.size()) {
+                final TilesStrip strip = searchCache.get(insertionPoint);
+                if (strip.covers(latitude)) {
+                    // we have found an existing strip
+                    return strip;
+                }
+            }
+
+            // no existing strip covers the specified latitude, we need to create a new one
+            final TilesStrip strip = new TilesStrip(createTile(latitude, longitude));
+            searchCache.add(insertionPoint, strip);
+            return strip;
+
+        }
+
+    }
+
+    /** Interface for retrieving latitude. */
+    private interface LatitudeProvider {
+
+        /** Get latitude.
+         * @return latitude
+         */
+        double getLatitude();
+
+    }
+
+    /** Basic implementation of {@link LatitudeProvider}. */
+    private static class BasicLatitudeProvider implements LatitudeProvider {
+
+        /** Latitude. */
+        private final double latitude;
+
+        /** Simple constructor.
+         * @param latitude latitude
+         */
+        public BasicLatitudeProvider(final double latitude) {
+            this.latitude = latitude;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public double getLatitude() {
+            return latitude;
+        }
+
+    }
+
+    /** Interface for retrieving longitude. */
+    private interface LongitudeProvider {
+
+        /** Get longitude.
+         * @return longitude
+         */
+        double getLongitude();
+
+    }
+
+    /** Basic implementation of {@link LongitudeProvider}. */
+    private static class BasicLongitudeProvider implements LongitudeProvider {
+
+        /** Longitude. */
+        private final double longitude;
+
+        /** Simple constructor.
+         * @param longitude longitude
+         */
+        public BasicLongitudeProvider(final double longitude) {
+            this.longitude = longitude;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public double getLongitude() {
+            return longitude;
+        }
+
+    }
+
+    /** Strip of tiles for a given latitude. */
+    private class TilesStrip implements LatitudeProvider {
+
+        /** Minimum latitude. */
+        private final double minLatitude;
+
+        /** Maximum latitude. */
+        private final double maxLatitude;
+
+        /** Tiles list. */
+        private final List<TileDecorator> tiles;
+
+        /** Simple constructor.
+         * @param tile first tile to insert in the strip
+         */
+        public TilesStrip(final T tile) {
+            minLatitude = tile.getMinimumLatitude();
+            maxLatitude = minLatitude + tile.getLatitudeRows() * tile.getLatitudeStep();
+            tiles       = new ArrayList<TileDecorator>();
+            tiles.add(new TileDecorator(tile));
+        }
+
+        /** Check if the strip covers a specified latitude.
+         * @param latitude latitude to check
+         * @return true if the strip covers the latitude
+         */
+        public boolean covers(final double latitude) {
+            return (minLatitude <= latitude) && (maxLatitude >= latitude);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public double getLatitude() {
+            return maxLatitude;
+        }
+
+        /** Get a tile covering a ground point.
+         * @param latitude ground point latitude
+         * @param longitude ground point longitude
+         * @return strip covering the ground point
+         */
+        public T getTile(final double latitude, final double longitude) {
+
+            // look for a tile at the specified longitude
+            final int index = Collections.binarySearch(tiles, new BasicLongitudeProvider(longitude),
+                                                       new LongitudeComparator());
+            if (index >= 0) {
+                // rare case, the longitude is an exact maximum longitude for a tile
+                return tiles.get(index).getTile();
+            } else {
+
+                final int insertionPoint  = -(index + 1);
+
+                if (insertionPoint < tiles.size()) {
+                    final T tile = tiles.get(insertionPoint).getTile();
+                    if (tile.covers(latitude, longitude)) {
+                        // we have found an existing tile
+                        return tile;
+                    }
+                }
+
+                // no existing tile covers the specified ground point, we need to create a new one
+                final T tile = createTile(latitude, longitude);
+                tiles.add(insertionPoint, new TileDecorator(tile));
+                return tile;
+            }
+
+        }
+
+        /** Remove a tile from the strip.
+         * @param tile tile to remove
+         * @return true if the tile has been removed
+         */
+        public boolean removeTile(final Tile tile) {
+            for (final Iterator<TileDecorator> iterator = tiles.iterator(); iterator.hasNext();) {
+                if (iterator.next().getTile() == tile) {
+                    iterator.remove();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+    }
+
+    /** Decorator for tiles, implementing {@link LongitudeProvider}. */
+    private class TileDecorator implements LongitudeProvider {
+
+        /** Underlying tile. */
+        private final T tile;
+
+        /** Simple constructor.
+         * @param tile tile to decorate
+         */
+        public TileDecorator(final T tile) {
+            this.tile = tile;
+        }
+
+        /** Get the underlying tile.
+         * @return underlying tile
+         */
+        public T getTile() {
+            return tile;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public double getLongitude() {
+            return tile.getMinimumLongitude() + tile.getLongitudeColumns() * tile.getLongitudeStep();
+        }
+
+    }
+
+    /** Comparator for sorting with respect to latitude. */
+    private static class LatitudeComparator implements Comparator<LatitudeProvider> {
+
+        /** {@inheritDoc} */
+        @Override
+        public int compare(final LatitudeProvider o1, final LatitudeProvider o2) {
+            return Double.compare(o1.getLatitude(), o2.getLatitude());
+        }
+        
+    }
+
+    /** Comparator for sorting with respect to longitude. */
+    private static class LongitudeComparator implements Comparator<LongitudeProvider> {
+
+        /** {@inheritDoc} */
+        @Override
+        public int compare(final LongitudeProvider o1, final LongitudeProvider o2) {
+            return Double.compare(o1.getLongitude(), o2.getLongitude());
+        }
+        
+    }
+
+}
diff --git a/rugged-core/src/test/java/orekit/rugged/core/dem/ConstantElevationUpdater.java b/rugged-core/src/test/java/orekit/rugged/core/dem/ConstantElevationUpdater.java
new file mode 100644
index 00000000..44777d7e
--- /dev/null
+++ b/rugged-core/src/test/java/orekit/rugged/core/dem/ConstantElevationUpdater.java
@@ -0,0 +1,46 @@
+/* Copyright 2013-2014 CS Systèmes d'Information
+ * Licensed to CS Systèmes d'Information (CS) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * CS licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package orekit.rugged.core.dem;
+
+import org.apache.commons.math3.util.FastMath;
+import org.orekit.rugged.api.TileUpdater;
+import org.orekit.rugged.api.UpdatableTile;
+
+public class ConstantElevationUpdater implements TileUpdater {
+
+    private double size;
+    private int    n;
+    private double elevation;
+
+    public ConstantElevationUpdater(double size, int n, double elevation) {
+        this.size      = size;
+        this.n         = n;
+        this.elevation = elevation;
+    }
+
+    public void updateTile(double latitude, double longitude, UpdatableTile tile) {
+        tile.setGeometry(size * FastMath.floor(latitude / size),
+                         size * FastMath.floor(longitude / size),
+                         size / n, size / n, n, n);
+        for (int i = 0; i < n; ++i) {
+            for (int j = 0; j < n; ++j) {
+                tile.setElevation(i, j, elevation);
+            }
+        }
+    }
+
+}
diff --git a/rugged-core/src/test/java/orekit/rugged/core/dem/CountingFactory.java b/rugged-core/src/test/java/orekit/rugged/core/dem/CountingFactory.java
new file mode 100644
index 00000000..157d4505
--- /dev/null
+++ b/rugged-core/src/test/java/orekit/rugged/core/dem/CountingFactory.java
@@ -0,0 +1,39 @@
+/* Copyright 2013-2014 CS Systèmes d'Information
+ * Licensed to CS Systèmes d'Information (CS) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * CS licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package orekit.rugged.core.dem;
+
+import org.orekit.rugged.core.dem.SimpleTile;
+import org.orekit.rugged.core.dem.TileFactory;
+
+public class CountingFactory implements TileFactory<SimpleTile> {
+
+    private int count;
+
+    public CountingFactory() {
+        count = 0;
+    }
+
+    public SimpleTile createTile() {
+        ++count;
+        return new SimpleTile();
+    }
+
+    public int getCount() {
+        return count;
+    }
+
+}
diff --git a/rugged-core/src/test/java/orekit/rugged/core/dem/TilesCacheTest.java b/rugged-core/src/test/java/orekit/rugged/core/dem/TilesCacheTest.java
new file mode 100644
index 00000000..4a7a8907
--- /dev/null
+++ b/rugged-core/src/test/java/orekit/rugged/core/dem/TilesCacheTest.java
@@ -0,0 +1,151 @@
+/* Copyright 2013-2014 CS Systèmes d'Information
+ * Licensed to CS Systèmes d'Information (CS) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * CS licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package orekit.rugged.core.dem;
+
+import org.apache.commons.math3.random.RandomGenerator;
+import org.apache.commons.math3.random.Well19937a;
+import org.apache.commons.math3.util.FastMath;
+import org.junit.Assert;
+import org.junit.Test;
+import org.orekit.rugged.core.dem.SimpleTile;
+import org.orekit.rugged.core.dem.TilesCache;
+
+public class TilesCacheTest {
+
+    @Test
+    public void testSingleTile() {
+        CountingFactory factory = new CountingFactory();
+        TilesCache<SimpleTile> cache = new TilesCache<SimpleTile>(factory,
+                new ConstantElevationUpdater(FastMath.toRadians(3.0), 10, 10.0), 1000);
+        SimpleTile tile = cache.getTile(FastMath.toRadians(-23.2), FastMath.toRadians(137.5));
+        Assert.assertEquals(1, factory.getCount());
+        Assert.assertEquals(-24.0, FastMath.toDegrees(tile.getMinimumLatitude()),  1.0e-10);
+        Assert.assertEquals(135.0, FastMath.toDegrees(tile.getMinimumLongitude()), 1.0e-10);
+        Assert.assertEquals(  0.3, FastMath.toDegrees(tile.getLatitudeStep()),     1.0e-10);
+        Assert.assertEquals(  0.3, FastMath.toDegrees(tile.getLongitudeStep()),    1.0e-10);
+    }
+
+    @Test
+    public void testEviction() {
+        CountingFactory factory = new CountingFactory();
+        TilesCache<SimpleTile> cache = new TilesCache<SimpleTile>(factory,
+                new ConstantElevationUpdater(FastMath.toRadians(1.0), 10, 10.0), 12);
+
+        // fill up the 12 tiles we can keep in cache
+        for (int i = 0; i < 4; ++i) {
+            for (int j = 0; j < 3; ++j) {
+                cache.getTile(FastMath.toRadians(0.5 + j), FastMath.toRadians(0.5 + i));
+            }
+        }
+        Assert.assertEquals(12, factory.getCount());
+
+        // keep using the same tiles for a while
+        RandomGenerator generator = new Well19937a(0xf556baa5977435c5l);
+        for (int i = 0; i < 10000; ++i) {
+            double lat = 3.0 * generator.nextDouble();
+            double lon = 4.0 * generator.nextDouble();
+            cache.getTile(FastMath.toRadians(lat), FastMath.toRadians(lon));
+        }
+        Assert.assertEquals(12, factory.getCount());
+
+        // ask for one point outside of the covered area, to evict the (0.0, 0.0) tile
+        cache.getTile(FastMath.toRadians(20.5), FastMath.toRadians(30.5));
+        Assert.assertEquals(13, factory.getCount());
+
+        // ask again for one point in the evicted tile which must be reallocated
+        cache.getTile(FastMath.toRadians(0.5), FastMath.toRadians(0.5));
+        Assert.assertEquals(14, factory.getCount());
+
+        // the 13th allocated tile should still be there
+        cache.getTile(FastMath.toRadians(20.5), FastMath.toRadians(30.5));
+        Assert.assertEquals(14, factory.getCount());
+
+        // evict all the tiles, goind to a completely different zone
+        for (int i = 0; i < 4; ++i) {
+            for (int j = 0; j < 3; ++j) {
+                cache.getTile(FastMath.toRadians(40.5 + i), FastMath.toRadians(90.5 + j));
+            }
+        }
+        Assert.assertEquals(26, factory.getCount());
+
+    }
+
+    @Test
+    public void testExactEnd() {
+        CountingFactory factory = new CountingFactory();
+        TilesCache<SimpleTile> cache =
+                new TilesCache<SimpleTile>(factory,
+                                           new ConstantElevationUpdater(0.125, 8, 10.0),
+                                           12);
+
+        SimpleTile regularTile = cache.getTile(0.2, 0.6);
+        Assert.assertEquals(1, factory.getCount());
+        Assert.assertEquals(0.125,    regularTile.getMinimumLatitude(),  1.0e-10);
+        Assert.assertEquals(0.5,      regularTile.getMinimumLongitude(), 1.0e-10);
+        Assert.assertEquals(0.015625, regularTile.getLatitudeStep(),     1.0e-10);
+        Assert.assertEquals(0.015625, regularTile.getLongitudeStep(),    1.0e-10);
+
+        SimpleTile tileAtEnd = cache.getTile(0.250, 0.625);
+        Assert.assertEquals(1, factory.getCount());
+        Assert.assertEquals(0.125,    tileAtEnd.getMinimumLatitude(),  1.0e-10);
+        Assert.assertEquals(0.5,      tileAtEnd.getMinimumLongitude(), 1.0e-10);
+        Assert.assertEquals(0.015625, tileAtEnd.getLatitudeStep(),     1.0e-10);
+        Assert.assertEquals(0.015625, tileAtEnd.getLongitudeStep(),    1.0e-10);
+
+    }
+
+    @Test
+    public void testNonContiguousFill() {
+        CountingFactory factory = new CountingFactory();
+        TilesCache<SimpleTile> cache =
+                new TilesCache<SimpleTile>(factory,
+                                           new ConstantElevationUpdater(FastMath.toRadians(1.0), 10, 10.0),
+                                           16);
+
+        cache.getTile(FastMath.toRadians(1.5), FastMath.toRadians(0.5));
+        cache.getTile(FastMath.toRadians(3.5), FastMath.toRadians(2.5));
+        cache.getTile(FastMath.toRadians(2.5), FastMath.toRadians(3.5));
+        cache.getTile(FastMath.toRadians(3.5), FastMath.toRadians(3.5));
+        cache.getTile(FastMath.toRadians(1.5), FastMath.toRadians(3.5));
+        cache.getTile(FastMath.toRadians(1.5), FastMath.toRadians(1.5));
+        cache.getTile(FastMath.toRadians(3.5), FastMath.toRadians(1.5));
+        cache.getTile(FastMath.toRadians(2.5), FastMath.toRadians(1.5));
+        cache.getTile(FastMath.toRadians(0.5), FastMath.toRadians(3.5));
+        cache.getTile(FastMath.toRadians(1.5), FastMath.toRadians(2.5));
+        cache.getTile(FastMath.toRadians(2.5), FastMath.toRadians(2.5));
+        cache.getTile(FastMath.toRadians(0.5), FastMath.toRadians(2.5));
+        cache.getTile(FastMath.toRadians(3.5), FastMath.toRadians(0.5));
+        cache.getTile(FastMath.toRadians(0.5), FastMath.toRadians(1.5));
+        cache.getTile(FastMath.toRadians(2.5), FastMath.toRadians(0.5));
+        cache.getTile(FastMath.toRadians(0.5), FastMath.toRadians(0.5));
+        Assert.assertEquals(16, factory.getCount());
+
+        // keep using the same tiles for a while
+        RandomGenerator generator = new Well19937a(0x1c951160de55c9d5l);
+        for (int i = 0; i < 10000; ++i) {
+            double lat = 3.0 * generator.nextDouble();
+            double lon = 4.0 * generator.nextDouble();
+            cache.getTile(FastMath.toRadians(lat), FastMath.toRadians(lon));
+        }
+        Assert.assertEquals(16, factory.getCount());
+
+        cache.getTile(FastMath.toRadians(-30.5), FastMath.toRadians(2.5));
+        Assert.assertEquals(17, factory.getCount());
+
+    }
+
+}
-- 
GitLab