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