MinMaxTreeTile.java
/* Copyright 2013-2022 CS GROUP
* Licensed to CS GROUP (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.intersection.duvenhage;
import org.hipparchus.util.FastMath;
import org.orekit.rugged.errors.DumpManager;
import org.orekit.rugged.raster.SimpleTile;
import org.orekit.rugged.utils.MaxSelector;
import org.orekit.rugged.utils.MinSelector;
import org.orekit.rugged.utils.Selector;
/** Implementation of a {@link org.orekit.rugged.raster.Tile} with a min/max kd tree.
* <p>
* A n level min/max kd-tree contains sub-tiles merging individual cells
* together from coarse-grained (at level 0) to fine-grained (at level n-1).
* Level n-1, which is the deepest one, is computed from the raw cells by
* merging adjacent cells pairs columns (i.e. cells at indices (i, 2j)
* and (i, 2j+1) are merged together by computing and storing the minimum
* and maximum in a sub-tile. Level n-1 therefore has the same number of rows
* but half the number of columns of the raw tile, and its sub-tiles are
* 1 cell high and 2 cells wide. Level n-2 is computed from level n-1 by
* merging sub-tiles rows. Level n-2 therefore has half the number of rows
* and half the number of columns of the raw tile, and its sub-tiles are
* 2 cells high and 2 cells wide. Level n-3 is again computed by merging
* columns, level n-4 merging rows and so on. As depth decreases, the number
* of sub-tiles decreases and their size increase. Level 0 is reached when
* there is only either one row or one column of large sub-tiles.
* </p>
* <p>
* During the merging process, if at some stage there is an odd number of
* rows or columns, then the last sub-tile at next level will not be computed
* by merging two rows/columns from the current level, but instead computed by
* simply copying the last single row/column. The process is therefore well
* defined for any raw tile initial dimensions. A direct consequence is that
* the dimension of the sub-tiles in the last row or column may be smaller than
* the dimension of regular sub-tiles.
* </p>
* <p>
* If we consider for example a tall 107 ⨉ 19 raw tile, the min/max kd-tree will
* have 9 levels:
* </p>
*
* <table summary="" border="0">
* <tr style="background-color:#EEEEFF;">
* <td>Level</td> <td>Number of sub-tiles</td> <td>Regular sub-tiles dimension</td></tr>
* <tr> <td align="center">8</td> <td align="center">107 ⨉ 10</td> <td align="center"> 1 ⨉ 2</td>
* <tr> <td align="center">7</td> <td align="center"> 54 ⨉ 10</td> <td align="center"> 2 ⨉ 2</td>
* <tr> <td align="center">6</td> <td align="center"> 54 ⨉ 5</td> <td align="center"> 2 ⨉ 4</td>
* <tr> <td align="center">5</td> <td align="center"> 27 ⨉ 5</td> <td align="center"> 4 ⨉ 4</td>
* <tr> <td align="center">4</td> <td align="center"> 27 ⨉ 3</td> <td align="center"> 4 ⨉ 8</td>
* <tr> <td align="center">3</td> <td align="center"> 14 ⨉ 3</td> <td align="center"> 8 ⨉ 8</td>
* <tr> <td align="center">2</td> <td align="center"> 14 ⨉ 2</td> <td align="center"> 8 ⨉ 16</td>
* <tr> <td align="center">1</td> <td align="center"> 7 ⨉ 2</td> <td align="center">16 ⨉ 16</td>
* <tr> <td align="center">0</td> <td align="center"> 7 ⨉ 1</td> <td align="center">16 ⨉ 32</td>
* </table>
* <p>
* @see MinMaxTreeTileFactory
* @author Luc Maisonobe
*/
public class MinMaxTreeTile extends SimpleTile {
/** Raw elevations. */
private double[] raw;
/** Min kd-tree. */
private double[] minTree;
/** Max kd-tree. */
private double[] maxTree;
/** Start indices of tree levels. */
private int[] start;
/** Simple constructor.
* <p>
* Creates an empty tile.
* </p>
*/
MinMaxTreeTile() {
}
/** {@inheritDoc} */
@Override
protected void processUpdatedElevation(final double[] elevations) {
raw = elevations;
final int nbRows = getLatitudeRows();
final int nbCols = getLongitudeColumns();
// set up the levels
final int size = setLevels(0, nbRows, nbCols);
minTree = new double[size];
maxTree = new double[size];
// compute min/max trees
if (start.length > 0) {
final double[] preprocessed = new double[raw.length];
preprocess(preprocessed, raw, nbRows, nbCols, MinSelector.getInstance());
applyRecursively(minTree, start.length - 1, nbRows, nbCols, MinSelector.getInstance(), preprocessed, 0);
preprocess(preprocessed, raw, nbRows, nbCols, MaxSelector.getInstance());
applyRecursively(maxTree, start.length - 1, nbRows, nbCols, MaxSelector.getInstance(), preprocessed, 0);
}
}
/** Get the number of kd-tree levels (not counting raw elevations).
* @return number of kd-tree levels
* @see #getMinElevation(int, int, int)
* @see #getMaxElevation(int, int, int)
* @see #getMergeLevel(int, int, int, int)
*/
public int getLevels() {
return start.length;
}
/** Get the minimum elevation at some level tree.
* <p>
* Note that the min elevation is <em>not</em> computed
* only at cell center, but considering that it is interpolated
* considering also Eastwards and Northwards neighbors, and extends
* up to the center of these neighbors. As an example, lets consider
* four neighboring cells in some Digital Elevation Model:
* <table summary="" border="0" cellpadding="5" style="background-color:#f5f5dc;">
* <tr><th style="background-color:#c9d5c9;">j+1</th><td>11</td><td>10</td></tr>
* <tr><th style="background-color:#c9d5c9;">j</th><td>12</td><td>11</td></tr>
* <tr style="background-color:#c9d5c9;"><th>j/i</th><th>i</th><th>i+1</th></tr>
* </table>
* When we interpolate elevation at a point located slightly South-West
* to the center of the (i+1, j+1) cell, we use all four cells in the
* interpolation, and we will get a result very close to 10 if we start
* close to (i+1, j+1) cell center. As the min value for this interpolation
* is stored at (i, j) indices, this implies that {@code getMinElevation(i,
* j, l)} must return 10 if l is chosen such that the sub-tile at
* tree level l includes cell (i,j) but not cell (i+1, j+1). In other words,
* interpolation implies sub-tile boundaries are overshoot by one column to
* the East and one row to the North when computing min.
*
* @param i row index of the cell
* @param j column index of the cell
* @param level tree level
* @return minimum value that can be reached when interpolating elevation
* in the sub-tile
* @see #getLevels()
* @see #getMaxElevation(int, int, int)
* @see #getMergeLevel(int, int, int, int)
*/
public double getMinElevation(final int i, final int j, final int level) {
// compute indices in level merged array
final int k = start.length - level;
final int rowShift = k / 2;
final int colShift = (k + 1) / 2;
final int levelI = i >> rowShift;
final int levelJ = j >> colShift;
final int levelC = 1 + ((getLongitudeColumns() - 1) >> colShift);
if (DumpManager.isActive()) {
final int[] min = locateMin(i, j, level);
final int index = min[0] * getLongitudeColumns() + min[1];
DumpManager.dumpTileCell(this, min[0], min[1], raw[index]);
if (index + getLongitudeColumns() < raw.length) {
DumpManager.dumpTileCell(this, min[0] + 1, min[1], raw[index + getLongitudeColumns()]);
}
if (index + 1 < raw.length) {
DumpManager.dumpTileCell(this, min[0], min[1] + 1, raw[index + 1]);
}
if (index + getLongitudeColumns() + 1 < raw.length) {
DumpManager.dumpTileCell(this, min[0] + 1, min[1] + 1, raw[index + getLongitudeColumns() + 1]);
}
}
return minTree[start[level] + levelI * levelC + levelJ];
}
/** Get the maximum elevation at some level tree.
* <p>
* Note that the max elevation is <em>not</em> computed
* only at cell center, but considering that it is interpolated
* considering also Eastwards and Northwards neighbors, and extends
* up to the center of these neighbors. As an example, lets consider
* four neighboring cells in some Digital Elevation Model:
* <table summary="" border="0" cellpadding="5" style="background-color:#f5f5dc;">
* <tr><th style="background-color:#c9d5c9;">j+1</th><td>11</td><td>12</td></tr>
* <tr><th style="background-color:#c9d5c9;">j</th><td>10</td><td>11</td></tr>
* <tr style="background-color:#c9d5c9;"><th>j/i</th><th>i</th><th>i+1</th></tr>
* </table>
* When we interpolate elevation at a point located slightly South-West
* to the center of the (i+1, j+1) cell, we use all four cells in the
* interpolation, and we will get a result very close to 12 if we start
* close to (i+1, j+1) cell center. As the max value for this interpolation
* is stored at (i, j) indices, this implies that {@code getMaxElevation(i,
* j, l)} must return 12 if l is chosen such that the sub-tile at
* tree level l includes cell (i,j) but not cell (i+1, j+1). In other words,
* interpolation implies sub-tile boundaries are overshoot by one column to
* the East and one row to the North when computing max.
*
* @param i row index of the cell
* @param j column index of the cell
* @param level tree level
* @return maximum value that can be reached when interpolating elevation
* in the sub-tile
* @see #getLevels()
* @see #getMinElevation(int, int, int)
* @see #getMergeLevel(int, int, int, int)
*/
public double getMaxElevation(final int i, final int j, final int level) {
// compute indices in level merged array
final int k = start.length - level;
final int rowShift = k / 2;
final int colShift = (k + 1) / 2;
final int levelI = i >> rowShift;
final int levelJ = j >> colShift;
final int levelC = 1 + ((getLongitudeColumns() - 1) >> colShift);
if (DumpManager.isActive()) {
final int[] max = locateMax(i, j, level);
final int index = max[0] * getLongitudeColumns() + max[1];
DumpManager.dumpTileCell(this, max[0], max[1], raw[index]);
if (index + getLongitudeColumns() < raw.length) {
DumpManager.dumpTileCell(this, max[0] + 1, max[1], raw[index + getLongitudeColumns()]);
}
if (index + 1 < raw.length) {
DumpManager.dumpTileCell(this, max[0], max[1] + 1, raw[index + 1]);
}
if (index + getLongitudeColumns() + 1 < raw.length) {
DumpManager.dumpTileCell(this, max[0] + 1, max[1] + 1, raw[index + getLongitudeColumns() + 1]);
}
}
return maxTree[start[level] + levelI * levelC + levelJ];
}
/** Locate the cell at which min elevation is reached for a specified level.
* <p>
* Min is computed with respect to the continuous interpolated elevation, which
* takes four neighboring cells into account. This implies that the cell at which
* min value is reached for some level is either within the sub-tile for this level,
* or in some case it may be one column outside to the East or one row outside to
* the North. See {@link #getMinElevation()} for a more complete explanation.
* </p>
* @param i row index of the cell
* @param j column index of the cell
* @param level tree level of the sub-tile considered
* @return row/column indices of the cell at which min elevation is reached
*/
public int[] locateMin(final int i, final int j, final int level) {
return locateMinMax(i, j, level, MinSelector.getInstance(), minTree);
}
/** Locate the cell at which max elevation is reached for a specified level.
* <p>
* Max is computed with respect to the continuous interpolated elevation, which
* takes four neighboring cells into account. This implies that the cell at which
* max value is reached for some level is either within the sub-tile for this level,
* or in some case it may be one column outside to the East or one row outside to
* the North. See {@link #getMaxElevation()} for a more complete explanation.
* </p>
* @param i row index of the cell
* @param j column index of the cell
* @param level tree level of the sub-tile considered
* @return row/column indices of the cell at which min elevation is reached
*/
public int[] locateMax(final int i, final int j, final int level) {
return locateMinMax(i, j, level, MaxSelector.getInstance(), maxTree);
}
/** Locate the cell at which min/max elevation is reached for a specified level.
* @param i row index of the cell
* @param j column index of the cell
* @param level tree level of the sub-tile considered
* @param selector min/max selector to use
* @param tree min/max tree to use
* @return row/column indices of the cell at which min/max elevation is reached
*/
private int[] locateMinMax(final int i, final int j, final int level,
final Selector selector, final double[] tree) {
final int k = start.length - level;
int rowShift = k / 2;
int colShift = (k + 1) / 2;
int levelI = i >> rowShift;
int levelJ = j >> colShift;
int levelR = 1 + ((getLatitudeRows() - 1) >> rowShift);
int levelC = 1 + ((getLongitudeColumns() - 1) >> colShift);
// track the cell ancestors from merged tree at specified level up to tree at level 1
for (int l = level + 1; l < start.length; ++l) {
if (isColumnMerging(l)) {
--colShift;
levelC = 1 + ((getLongitudeColumns() - 1) >> colShift);
levelJ = levelJ << 1;
if (levelJ + 1 < levelC) {
// the cell results from a regular merging of two columns
if (selector.selectFirst(tree[start[l] + levelI * levelC + levelJ + 1],
tree[start[l] + levelI * levelC + levelJ])) {
levelJ++;
}
}
} else {
--rowShift;
levelR = 1 + ((getLatitudeRows() - 1) >> rowShift);
levelI = levelI << 1;
if (levelI + 1 < levelR) {
// the cell results from a regular merging of two rows
if (selector.selectFirst(tree[start[l] + (levelI + 1) * levelC + levelJ],
tree[start[l] + levelI * levelC + levelJ])) {
levelI++;
}
}
}
}
// we are now at first merge level, which always results from a column merge
// or pre-processed data, which themselves result from merging four cells
// used in interpolation
// this imply the ancestor of min/max at (n, m) is one of
// (2n, m), (2n+1, m), (2n+2, m), (2n, m+1), (2n+1, m+1), (2n+2, m+1)
int selectedI = levelI;
int selectedJ = 2 * levelJ;
double selectedElevation = Double.NaN;
for (int n = 2 * levelJ; n < 2 * levelJ + 3; ++n) {
if (n < getLongitudeColumns()) {
for (int m = levelI; m < levelI + 2; ++m) {
if (m < getLatitudeRows()) {
final double elevation = raw[m * getLongitudeColumns() + n];
if (selector.selectFirst(elevation, selectedElevation)) {
selectedI = m;
selectedJ = n;
selectedElevation = elevation;
}
}
}
}
}
return new int[] {
selectedI, selectedJ
};
}
/** Get the deepest level at which two cells are merged in the same min/max sub-tile.
* @param i1 row index of first cell
* @param j1 column index of first cell
* @param i2 row index of second cell
* @param j2 column index of second cell
* @return deepest level at which two cells are merged in the same min/max sub-tile,
* or -1 if they are never merged in the same sub-tile
* @see #getLevels()
* @see #getMinElevation(int, int, int)
* @see #getMaxElevation(int, int, int)
*/
public int getMergeLevel(final int i1, final int j1, final int i2, final int j2) {
int largest = -1;
for (int level = 0; level < start.length; ++level) {
// compute indices in level merged array
final int k = start.length - level;
final int rowShift = k / 2;
final int colShift = (k + 1) / 2;
final int levelI1 = i1 >> rowShift;
final int levelJ1 = j1 >> colShift;
final int levelI2 = i2 >> rowShift;
final int levelJ2 = j2 >> colShift;
if (levelI1 != levelI2 || levelJ1 != levelJ2) {
return largest;
}
largest = level;
}
return largest;
}
/** Get the index of sub-tiles start rows crossed.
* <p>
* When going from one row to another row at some tree level,
* we cross sub-tiles boundaries. This method returns the index
* of these boundaries.
* </p>
* @param row1 starting row
* @param row2 ending row
* @param level tree level
* @return indices of rows crossed at sub-tiles boundaries, in crossing order,
* the endpoints <em>are</em> included (i.e. if {@code row1} or {@code row2} are
* boundary rows, they will be in returned array)
*/
public int[] getCrossedBoundaryRows(final int row1, final int row2, final int level) {
// number of rows in each sub-tile
final int rows = 1 << ((start.length - level) / 2);
// build the crossings in ascending order
final int min = FastMath.min(row1, row2);
final int max = FastMath.max(row1, row2) + 1;
return buildCrossings((min + rows - 1) - ((min + rows - 1) % rows), max, rows,
row1 <= row2);
}
/** Get the index of sub-tiles start columns crossed.
* <p>
* When going from one column to another column at some tree level,
* we cross sub-tiles boundaries. This method returns the index
* of these boundaries.
* </p>
* @param column1 starting column
* @param column2 ending column (excluded)
* @param level tree level
* @return indices of columns crossed at sub-tiles boundaries, in crossing order,
* the endpoints <em>are</em> included (i.e. if {@code column1} or {@code column2} are
* boundary columns, they will be in returned array)
*/
public int[] getCrossedBoundaryColumns(final int column1, final int column2, final int level) {
// number of columns in each sub-tile
final int columns = 1 << ((start.length + 1 - level) / 2);
// build the crossings in ascending order
final int min = FastMath.min(column1, column2);
final int max = FastMath.max(column1, column2) + 1;
return buildCrossings((min + columns - 1) - ((min + columns - 1) % columns), max, columns,
column1 <= column2);
}
/** Build crossings arrays.
* @param begin begin crossing index
* @param end end crossing index (excluded, if equal to begin, the array is empty)
* @param step crossing step
* @param ascending if true, the crossings must be in ascending order
* @return indices of rows or columns crossed at sub-tiles boundaries, in crossing order
*/
private int[] buildCrossings(final int begin, final int end, final int step, final boolean ascending) {
// allocate array
final int n = FastMath.max(0, (end - begin + step + ((step > 0) ? -1 : +1)) / step);
final int[] crossings = new int[n];
// fill it up
int crossing = begin;
if (ascending) {
for (int i = 0; i < crossings.length; ++i) {
crossings[i] = crossing;
crossing += step;
}
} else {
for (int i = 0; i < crossings.length; ++i) {
crossings[crossings.length - 1 - i] = crossing;
crossing += step;
}
}
return crossings;
}
/** Check if the merging operation between level and level-1 is a column merging.
* @param level level to check
* @return true if the merging operation between level and level-1 is a column
* merging, false if is a row merging
*/
public boolean isColumnMerging(final int level) {
return (level & 0x1) == (start.length & 0x1);
}
/** Recursive setting of tree levels.
* <p>
* The following algorithms works for any array shape, even with
* rows or columns which are not powers of 2 or with one
* dimension much larger than the other. As an example, starting
* from a 107 ⨉ 19 array, we get the following 9 levels, for a
* total of 2187 elements in each tree:
* </p>
* <p>
* <table border="0">
* <tr BGCOLOR="#EEEEFF">
* <td>Level</td> <td>Dimension</td> <td>Start index</td> <td>End index (inclusive)</td></tr>
* <tr> <td>0</td> <td> 7 ⨉ 1</td> <td> 0</td> <td> 6</td> </tr>
* <tr> <td>1</td> <td> 7 ⨉ 2</td> <td> 7</td> <td> 20</td> </tr>
* <tr> <td>2</td> <td> 14 ⨉ 2</td> <td> 21</td> <td> 48</td> </tr>
* <tr> <td>3</td> <td> 14 ⨉ 3</td> <td> 49</td> <td> 90</td> </tr>
* <tr> <td>4</td> <td> 27 ⨉ 3</td> <td> 91</td> <td>171</td> </tr>
* <tr> <td>5</td> <td> 27 ⨉ 5</td> <td> 172</td> <td>306</td> </tr>
* <tr> <td>6</td> <td> 54 ⨉ 5</td> <td> 307</td> <td>576</td> </tr>
* <tr> <td>7</td> <td> 54 ⨉ 10</td> <td> 577</td> <td>1116</td> </tr>
* <tr> <td>8</td> <td>107 ⨉ 10</td> <td>1117</td> <td>2186</td> </tr>
* </table>
* </p>
* @param stage number of merging stages
* @param stageRows number of rows at current stage
* @param stageColumns number of columns at current stage
* @return size cumulative size from root to current level
*/
private int setLevels(final int stage, final int stageRows, final int stageColumns) {
if (stageRows == 1 || stageColumns == 1) {
// we have found root, stop recursion
start = new int[stage];
if (stage > 0) {
start[0] = 0;
}
return stageRows * stageColumns;
}
final int size;
if ((stage & 0x1) == 0) {
// columns merging
size = setLevels(stage + 1, stageRows, (stageColumns + 1) / 2);
} else {
// rows merging
size = setLevels(stage + 1, (stageRows + 1) / 2, stageColumns);
}
if (stage > 0) {
// store current level characteristics
start[start.length - stage] = size;
return size + stageRows * stageColumns;
} else {
// we don't count the elements at stage 0 as they are not stored in the
// min/max trees (they correspond to the raw elevation, without merging)
return size;
}
}
/** Preprocess recursive application of a function.
* <p>
* At start, the min/max should be computed for each cell using the four corners values.
* </p>
* @param preprocessed preprocessed array to fill up
* @param elevations raw elevations te preprocess
* @param nbRows number of rows
* @param nbCols number of columns
* @param selector selector to use
*/
private void preprocess(final double[] preprocessed, final double[] elevations,
final int nbRows, final int nbCols,
final Selector selector) {
int k = 0;
for (int i = 0; i < nbRows - 1; ++i) {
// regular elements with both a column at right and a row below
for (int j = 0; j < nbCols - 1; ++j) {
preprocessed[k] = selector.select(selector.select(elevations[k], elevations[k + 1]),
selector.select(elevations[k + nbCols], elevations[k + nbCols + 1]));
k++;
}
// last column elements, lacking a right column
preprocessed[k] = selector.select(elevations[k], elevations[k + nbCols]);
k++;
}
// last row elements, lacking a below row
for (int j = 0; j < nbCols - 1; ++j) {
preprocessed[k] = selector.select(elevations[k], elevations[k + 1]);
k++;
}
// last element
preprocessed[k] = elevations[k];
}
/** Recursive application of a function.
* @param tree tree to fill-up with the recursive applications
* @param level current level
* @param levelRows number of rows at current level
* @param levelColumns number of columns at current level
* @param selector to apply
* @param base base array from which function arguments are drawn
* @param first index of the first element to consider in base array
*/
private void applyRecursively(final double[] tree,
final int level, final int levelRows, final int levelColumns,
final Selector selector,
final double[] base, final int first) {
if (isColumnMerging(level + 1)) {
// merge columns pairs
int iTree = start[level];
int iBase = first;
final int nextColumns = (levelColumns + 1) / 2;
final boolean odd = (levelColumns & 0x1) != 0;
final int jEnd = odd ? nextColumns - 1 : nextColumns;
for (int i = 0; i < levelRows; ++i) {
// regular pairs
for (int j = 0; j < jEnd; ++j) {
tree[iTree++] = selector.select(base[iBase], base[iBase + 1]);
iBase += 2;
}
if (odd) {
// last column
tree[iTree++] = base[iBase++];
}
}
if (level > 0) {
applyRecursively(tree, level - 1, levelRows, nextColumns, selector, tree, start[level]);
}
} else {
// merge rows pairs
int iTree = start[level];
int iBase = first;
final int nextRows = (levelRows + 1) / 2;
final boolean odd = (levelRows & 0x1) != 0;
final int iEnd = odd ? nextRows - 1 : nextRows;
// regular pairs
for (int i = 0; i < iEnd; ++i) {
for (int j = 0; j < levelColumns; ++j) {
tree[iTree++] = selector.select(base[iBase], base[iBase + levelColumns]);
iBase++;
}
iBase += levelColumns;
}
if (odd) {
// last row
System.arraycopy(base, iBase, tree, iTree, levelColumns);
}
if (level > 0) {
applyRecursively(tree, level - 1, nextRows, levelColumns, selector, tree, start[level]);
}
}
}
}