/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.image.io.mosaic;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Comparator;
import java.util.Collections;
import java.util.Iterator;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.IOException;
import static org.geotoolkit.image.io.mosaic.Tile.MASK;
/**
* List of tiles inside the bounding box of a bigger (or equals in size) tile. This class fills
* a similar purpose than RTree, except that we do not calculate any new bounding boxes. We try
* to fit children in existing tile bounds on the assumption that most tile layouts are already
* organized in some form pyramid.
* <p>
* The value of the inherited rectangle is the {@linkplain Tile#getAbsoluteRegion absolute region}
* of the tile, computed and stored once for ever for efficiency during searches. This class extends
* {@link Rectangle} for pure opportunist reasons, in order to reduce the amount of object created
* (because we will have thousands of TreeNodes) and for direct (no indirection, no virtual calls)
* invocation of {@link Rectangle} services. We authorize ourself this unrecommendable practice only
* because this class is not public. The inherited {@link Rectangle} should <string>never</strong>
* be modified by anyone outside this class.
* <p>
* Note that the {@link #compareTo} method is inconsistent with {@link #equals}. It should
* be considered as an implementation details exposed because this class is not public.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.04
*
* @since 2.5
* @module
*/
@SuppressWarnings("serial") // Not expected to be serialized.
final class GridNode extends TreeNode implements Comparable<GridNode> {
/**
* The horizontal and vertical size (in number of tiles) of a virtual tile. This is used only
* when the structure of the tiles given at construction time is {@linkplain #isFlat flat}. In
* such case, a few virtual tiles are created for performance raison. Each virtual tiles will
* contains at most {@value}×{@value} real tiles.
*/
private static final int GROUP_SIZE = 3;
/**
* The index, used for preserving order compared to the user-specified one.
*/
private final int index;
/**
* The subsampling as an unsigned short greater than zero.
* The value must be used in expressions like {@code subsampling & 0xFFFF}.
* <p>
* On {@link GridNode} creation, this is initialized to {@linkplain Tile#getSubsampling tile
* subsampling}. After the call to {@link #postTreeCreation}, the value is increased (never
* reduced) to the greatest subsampling found in all children. Because children usually have
* finer subsampling, this value is typically unmodified compared to its initial value.
*/
private short xSubsampling, ySubsampling;
/**
* Comparator for sorting tiles by decreasing subsamplings and area. The {@linkplain
* GridNode#GridNode(Tile[]) constructor} expects this order for inserting a tile into
* the smallest tile that can contains it. If two tiles have the same subsampling, then
* they are sorted by decreasing area in absolute coordinates.
* <p>
* If two tiles have the same subsampling and area, then their relative order is left
* unchanged on the basis that initial order, when sorted by {@link TileManager}, should
* be efficient for reading tiles sequentially.
*/
private static final Comparator<GridNode> PRE_PROCESSING = new Comparator<GridNode>() {
@Override public int compare(final GridNode n1, final GridNode n2) {
final int s1 = (n1.xSubsampling & MASK) * (n1.ySubsampling & MASK);
final int s2 = (n2.xSubsampling & MASK) * (n2.ySubsampling & MASK);
if (s1 > s2) return -1; // Greatest values first
if (s1 < s2) return +1;
final long a1 = (long) n1.width * (long) n1.height;
final long a2 = (long) n2.width * (long) n2.height;
if (a1 > a2) return -1;
if (a1 < a2) return +1;
return n1.index - n2.index;
}
};
/**
* Comparator for sorting tiles in the same order than the one specified at construction time.
* <p>
* This method is inconsistent with {@link #equals}. It is okay for our usage of it,
* which should be restricted to this {@link GridNode} package-privated class only.
*/
@Override
public int compareTo(final GridNode that) {
return index - that.index;
}
/**
* Creates a node for the specified bounds with no subsampling and no tile.
* This constructor is invoked for giving some depth to an initially flat tree.
*/
private GridNode(final Rectangle bounds) {
super(bounds);
index = -1;
}
/**
* Creates a node for a single tile.
*
* @param tile The tile.
* @param index The original index in the user-specified array.
* @throws IOException if an I/O operation was required and failed.
*/
private GridNode(final Tile tile, final int index) throws IOException {
super(tile.getAbsoluteRegion());
this.tile = tile;
final Dimension subsampling = tile.getSubsampling();
Tile.checkSubsampling(subsampling);
xSubsampling = Tile.toShort(subsampling.width);
ySubsampling = Tile.toShort(subsampling.height);
this.index = index;
}
/**
* Builds the root of a tree for the given tiles.
*
* @param tiles The tiles to be inserted in the tree.
* @throws IOException if an I/O operation was required and failed.
*/
public GridNode(final Tile[] tiles) throws IOException {
/*
* Sorts the TreeNode with biggest tree first (this is required for the algorithm building
* the tree). Note that the TreeNode array should be created before any sorting is applied,
* because its creation may involve disk reading and those reading are more efficient when
* performed in the tiles iteration order (assuming this array was sorted by TileManager).
*/
GridNode[] nodes = new GridNode[tiles.length];
for (int i=0; i<tiles.length; i++) {
nodes[i] = new GridNode(tiles[i], i);
}
Arrays.sort(nodes, PRE_PROCESSING);
/*
* If every tiles have the same subsampling, we are probably in the case where a set of
* input tiles, all having similar size, are given to MosaicImageWriter for creating a
* pyramid of images. The RTree created from such set of tiles will be very inefficient.
* Adds a couple of fictious nodes with greater area so that the code after this block
* can create a deeper tree structure. Note that this is a somewhat naive algorithm.
* The aim is not to create a sophesticated RTree here; it is just to atenuate the
* worst case scenario.
*/
final boolean isFlat = isFlat(nodes);
if (isFlat) {
nodes = prependTree(nodes);
}
/*
* Special case: checks if the first node contains all subsequent nodes. If this is true,
* then there is no need to keep the special root TreeNode with the tile field set to null.
* We can keep directly the first node instead. Note that this special case should NOT be
* extended further the first node, otherwise the tiles prior the retained node would be
* discarded.
*/
GridNode root = null;
if (nodes.length != 0) {
root = nodes[0];
for (int i=1; i<nodes.length; i++) {
if (!root.contains(nodes[i])) {
root = null;
break;
}
}
}
if (root != null) {
setBounds(root);
tile = root.tile;
index = root.index;
xSubsampling = root.xSubsampling;
ySubsampling = root.ySubsampling;
} else {
index = -1;
}
/*
* Now inserts every nodes in the tree. At first we try to add each node into the smallest
* parent that can contain it and align it on a grid. If the node can not be aligned, then
* we add it into the smallest parent regardless of alignment. The grid condition produces
* good results for TileLayout.CONSTANT_TILE_SIZE. However it may not work so well with
* random tiles (open issue).
*/
for (int i=(root != null ? 1 : 0); i<nodes.length; i++) {
final GridNode child = nodes[i];
final GridNode parent = smallest(child);
parent.addChild(child);
}
/*
* Calculates the bounds only for root node, if not already computed. We do not iterate
* down the tree since every children should have their bounds set to the tile bounds.
*/
if (root == null) {
assert (width | height) < 0 : this;
TreeNode child = firstChildren();
while (child != null) {
// No need to invoke setBounds for the first child since Rectangle.add(Rectangle)
// takes care of that if the width or height is negative (specified in javadoc).
add(child);
child = child.nextSibling();
}
}
if (!isFlat) {
splitOverlappingChildren(); // Must be after bounds calculation.
}
postTreeCreation();
assert checkValidity() : toTree();
}
/**
* Returns the smallest tree node containing the given region. This method assumes that
* {@code this} node, if non-empty, {@linkplain #contains contains} the given bounds.
* Note that the constructor may invoke this method from the root with an empty bounding
* box, which is valid.
* <p>
* This method tries to returns the smallest {@linkplain #isGridded gridded} node, if any.
* By "gridded" we mean a node that can align the given bounds on a grid. If there is no
* such node, then any node containing the bounds is returned.
*
* @param The bounds to check for inclusion.
* @return The smallest node, or {@code this} if none (never {@code null}).
*/
private GridNode smallest(final Rectangle bounds) {
long smallestArea;
boolean gridded;
if (isEmpty()) {
smallestArea = Long.MAX_VALUE;
gridded = false;
} else {
assert contains(bounds);
smallestArea = (long) width * (long) height;
gridded = isGridded(bounds);
}
GridNode smallest = this;
GridNode child = (GridNode) firstChildren();
while (child != null) {
if (child.contains(bounds)) {
final GridNode candidate = child.smallest(bounds);
final boolean cg = candidate.isGridded(bounds);
if (!gridded || cg) {
final long area = (long) candidate.width * (long) candidate.height;
if ((!gridded && cg) || (area < smallestArea)) {
// If the smallest node was not gridded while the candidate is gridded,
// retains the candidate unconditionally. Otherwise retains only if smaller.
smallestArea = area;
smallest = candidate;
gridded = cg;
}
}
}
child = (GridNode) child.nextSibling();
}
return smallest;
}
/**
* Returns {@code true} if the given child is layered on a grid in this node.
*/
private boolean isGridded(final Rectangle child) {
return width % child.width == 0 && (child.x - x) % child.width == 0 &&
height % child.height == 0 && (child.y - y) % child.height == 0;
}
/**
* If this node contains children at different subsampling and some of them overlap,
* creates new nodes which regroup every tiles having the same subsampling. This simple
* algorithm does exactly what we want for the simple case where the overlapping exists
* because the subsampling of some tiles are not a multiple of the subsampling of other
* tiles.
* <p>
* We do <strong>not</strong> try to do anything special for overlapping of tiles at the
* same subsampling, because we assume that the user already validated his input tiles.
* Sometime those tiles overlap a bit (for example tiles having a width of 1003 pixels
* while we expected exactly 1000 pixels) but the user considers those overlapping as
* negligible. Trying to "solve" such overlapping cause more problems than good.
*/
private void splitOverlappingChildren() {
assert isLeaf() || !isEmpty() : this; // Requires that bounds has been computed.
/*
* Process the children. We must do that first because it may change the list of
* children in this node. Once the children have been processed, we can check if
* there is any overlapping in this node and stop this method if there is none.
*/
GridNode child = (GridNode) firstChildren();
while (child != null) {
child.splitOverlappingChildren();
child = (GridNode) child.nextSibling();
}
if (isFlat() || !hasOverlaps()) {
return;
}
/*
* Move the list of children in a temporary array.
*/
final List<GridNode> toProcess = new LinkedList<>();
final List<GridNode> retained = new ArrayList<>();
child = (GridNode) firstChildren();
while (child != null) {
toProcess.add(child);
child = (GridNode) child.nextSibling();
}
removeChildren(); // Necessary in order to give children to other nodes.
/*
* For every tiles to process, copy in the "retained" list those having the same subsampling.
* The other tiles will be left in the "toProcess" list for examination in an other pass.
*/
while (!toProcess.isEmpty()) {
final Iterator<GridNode> it = toProcess.iterator();
child = it.next();
retained.add(child);
it.remove();
final short xSubsampling = child.xSubsampling;
final short ySubsampling = child.ySubsampling;
while (it.hasNext()) {
child = it.next();
if (child.xSubsampling == xSubsampling && child.ySubsampling == ySubsampling) {
retained.add(child);
it.remove();
}
}
/*
* Following assertion was enabled in a previous version:
*
* assert Collections.disjoint(toProcess, retained);
*
* It still conceptually applicable, but it would needs to be applied using
* identity comparison (==) rather than the equals(Object) method, because
* overviews generated by GDAL (for example) use the same Rectangle than the
* tile containing them.
*/
child = new GridNode(this);
assert child.isLeaf();
for (final GridNode r : retained) {
child.addChild(r);
}
retained.clear();
addChild(child);
}
}
/**
* Invoked when the tree construction is completed with every nodes assigned to its final
* parent. This method calculate the values that depend on the child hierarchy, including
* subsampling.
*/
private void postTreeCreation() {
GridNode child = (GridNode) firstChildren();
while (child != null) {
child.postTreeCreation();
if ((child.xSubsampling & MASK) > (xSubsampling & MASK)) xSubsampling = child.xSubsampling;
if ((child.ySubsampling & MASK) > (ySubsampling & MASK)) ySubsampling = child.ySubsampling;
child = (GridNode) child.nextSibling();
}
}
/**
* Returns {@code true} if at least one tile having the given subsampling or a finer one
* intersects the given region.
*
* @param region The region to look for, in "absolute" coordinates.
* @param subsampling The maximal subsampling to look for.
* @return {@code true} if at least one tile having the given subsampling or
* a finer one intersects the given region.
*/
public boolean intersects(final Rectangle region, final Dimension subsampling) {
if (intersects(region)) {
if (tile != null) {
final int xSubsampling, ySubsampling;
if (isLeaf()) {
// Slight optimization: if we are a leaf, x/ySubsampling are guaranteed
// to be equal to the value returned by Tile.getSubsampling().
xSubsampling = (this.xSubsampling & MASK);
ySubsampling = (this.ySubsampling & MASK);
} else {
final Dimension candidate = tile.getSubsampling();
xSubsampling = candidate.width;
ySubsampling = candidate.height;
}
if (xSubsampling <= subsampling.width && ySubsampling <= subsampling.height) {
return true;
}
}
GridNode child = (GridNode) firstChildren();
while (child != null) {
if (child.intersects(region, subsampling)) {
return true;
}
child = (GridNode) child.nextSibling();
}
}
return false;
}
/**
* Returns the subsampling along <var>x</var> axis.
*/
public int getXSubsampling() {
return xSubsampling & MASK;
}
/**
* Returns the subsampling along <var>y</var> axis.
*/
public int getYSubsampling() {
return ySubsampling & MASK;
}
/**
* Returns {@code true} if every direct children in this node use the same subsampling.
* This method does not looks recursively in children of children.
*
* @return {@code true} if every direct children in this node use the same subsampling.
*/
private boolean isFlat() {
GridNode child = (GridNode) firstChildren();
if (child != null) {
final short xSubsampling = child.xSubsampling;
final short ySubsampling = child.ySubsampling;
while ((child = (GridNode) child.nextSibling()) != null) {
if (child.xSubsampling != xSubsampling || child.ySubsampling != ySubsampling) {
return false;
}
}
}
return true;
}
/**
* Returns {@code true} if every nodes in the given array use the same subsampling.
*
* @param tiles The array of nodes to check for common subsampling.
* @return {@code true} if every nodes in the given array use the same subsampling.
*/
private static boolean isFlat(final GridNode[] nodes) {
if (nodes != null && nodes.length != 0) {
GridNode node = nodes[0];
final short xSubsampling = node.xSubsampling;
final short ySubsampling = node.ySubsampling;
for (int i=1; i<nodes.length; i++) {
node = nodes[i];
if (node.xSubsampling != xSubsampling || node.ySubsampling != ySubsampling) {
return false;
}
}
}
return true;
}
/**
* Inserts a tree of nodes without tiles before the nodes in the given array. This method is
* typically invoked for {@linkplain #isFlat flat} array of nodes only, which are the "worst
* case" scenario. This method tries to attenuate the effect of worst case scenario.
* <p>
* The order of nodes is significant. This method must prepend bigger nodes first, like what
* we would get if the nodes where associated with real tiles and the array sorted with the
* {@link #PRE_PROCESSING} comparator.
* <p>
* The current algorithm requires that all tiles are organized on a regular grid. If the given
* array does not meet this criterion, then we are better to not try to prepend anything (the
* only consequence is slower execution). If we tried to use a better algorithm in this method,
* we would be taking the path of a real RTree, which is the subject of many litterature and
* out of scope of this mosaic package (which basically assumes a pre-existing layout suitable
* for the mosaic needs).
*
* @param nodes The nodes for which to prepend a tree.
* @return The nodes with a tree prepend before them.
*
* @todo In its current form, this method is not quite useful since it does its job only in
* the cases where <code>GridTileManager</code> would have been used instead than the
* <code>TreeTileManager</code>. It still useful for assertions since this method is
* used in the context of <code>ComparedTileManager</code>.
* <p>
* This method could be made more useful by extending its scope beyond the cases handled
* by <code>GridTileManager</code>. We could accept larger tiles having a size which is
* a multiple of "normal" tiles.
*/
private static GridNode[] prependTree(GridNode[] nodes) {
/*
* Computes the bounds of the whole mosaic and get the size of the largest tiles. We select
* the size of largest tiles because the last row and the last column often contain cropped
* tiles, so the "normal" tiles are the one having the maximal size.
*/
final Rectangle mosaicBounds = new Rectangle(-1, -1);
int tileWidth = 0;
int tileHeight = 0;
for (final GridNode node : nodes) {
mosaicBounds.add(node);
if (node.width > tileWidth) tileWidth = node.width;
if (node.height > tileHeight) tileHeight = node.height;
}
if (mosaicBounds.isEmpty()) {
return nodes;
}
/*
* While uncommon, it may happen that the first row and the first column contain cropped
* tiles has well. In such case the (x,y) location of the upper-left tile may not be the
* (x,y) location of the whole grid: an offset may exist. The code below compute the
* maximal allowed offset.
*/
int xOffset = 0;
int yOffset = 0;
for (final GridNode node : nodes) {
if (node.x == mosaicBounds.x) {
int s = tileWidth - node.width;
if (s > xOffset) xOffset = s;
}
if (node.y == mosaicBounds.y) {
int s = tileHeight - node.height;
if (s > yOffset) yOffset = s;
}
}
/*
* The current algorithm requires that all tiles are organized on a regular grid. Check
* if this condition is meet. If an offset is allowed (as computed by the above code),
* try all possible offset until a suitable value is found.
*/
adjust: while (true) {
for (final GridNode node : nodes) {
if (node.width > tileWidth - (node.x - mosaicBounds.x) % tileWidth) {
if (--xOffset < 0) {
return nodes;
}
mosaicBounds.x--;
continue adjust;
}
}
break;
}
adjust: while (true) {
for (final GridNode node : nodes) {
if (node.height > tileHeight - (node.y - mosaicBounds.y) % tileHeight) {
if (--yOffset < 0) {
return nodes;
}
mosaicBounds.y--;
continue adjust;
}
}
break;
}
/*
* Compute the number of "group of tiles" (or "virtual tiles"), where each group encompass
* at most 3×3 real tiles. Then build an array of booleans which indicates, for each group
* of tiles, if at least one tile exists in this group.
*/
tileWidth *= GROUP_SIZE;
tileHeight *= GROUP_SIZE;
int numTileX = (mosaicBounds.width + (tileWidth - 1)) / tileWidth;
int numTileY = (mosaicBounds.height + (tileHeight - 1)) / tileHeight;
boolean[] exists = new boolean[numTileX * numTileY];
for (final GridNode node : nodes) {
final int nx = (node.x - mosaicBounds.x) / tileWidth;
final int ny = (node.y - mosaicBounds.y) / tileHeight;
exists[ny * numTileX + nx] = true;
}
/*
* Now create the "virtual" tiles having at least one real tile. The tiles are
* inserted in reverse order, with the biggest tiles added last.
*/
final Rectangle region = new Rectangle();
final List<GridNode> extra = new ArrayList<>();
while (exists.length > 1) {
region.width = tileWidth;
region.height = tileHeight;
for (int i=exists.length; --i>=0;) {
if (exists[i]) {
region.x = mosaicBounds.x + tileWidth * (i % numTileX);
region.y = mosaicBounds.y + tileHeight * (i / numTileX);
extra.add(new GridNode(region.intersection(mosaicBounds)));
}
}
/*
* At this point the virtual tiles have been created. The next step will be to
* create an other level of "virtual tiles" which are 3×3 bigger than the ones
* we just created. For doing this, we need to compact the "exists" array in a
* smaller array.
*/
tileWidth *= GROUP_SIZE;
tileHeight *= GROUP_SIZE;
final boolean[] oldArray = exists;
final int oldRowLength = numTileX;
numTileX = (numTileX + (GROUP_SIZE-1)) / GROUP_SIZE;
numTileY = (numTileY + (GROUP_SIZE-1)) / GROUP_SIZE;
exists = new boolean[numTileX * numTileY];
for (int i=0; i<oldArray.length; i++) {
if (oldArray[i]) {
final int ny = (i / oldRowLength) / GROUP_SIZE;
final int nx = (i % oldRowLength) / GROUP_SIZE;
exists[ny * numTileX + nx] = true;
}
}
}
/*
* Copies the "virtual tiles" at the beginning of the nodes array.
*/
final int n = extra.size();
if (n != 0) {
Collections.reverse(extra);
final GridNode[] oldArray = nodes;
nodes = extra.toArray(new GridNode[n + oldArray.length]);
System.arraycopy(oldArray, 0, nodes, n, oldArray.length);
}
return nodes;
}
}