/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.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.ListIterator; import java.awt.Dimension; import java.awt.Rectangle; import java.io.IOException; import static org.geotools.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 efficienty during searchs. 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. * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux */ @SuppressWarnings("serial") // Not expected to be serialized. final class GridNode extends TreeNode implements Comparable<GridNode> { /** * 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; /** * {@code true} if at least one tile overlaps an other tile in direct {@linkplain #children}. * As a special case, if a tile has exactly the same bounding box than an other tile, then we * do not consider it as an overlap. This is because those exact matchs are easy to handle by * {@link RTree}. */ private boolean overlaps; /** * {@code true} if the children fill completly this node bounds. */ private boolean dense; /** * Comparator for sorting tiles by descreasing 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 descreasing area in absolute coordinates. * <p> * If two tiles have the same subsampling and area, then their order is restored 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>() { 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. */ public int compareTo(final GridNode that) { return index - that.index; } /** * Creates a node for the specified bounds with no subsampling and no tile. */ 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(); xSubsampling = Tile.ensureStrictlyPositive(subsampling.width); ySubsampling = Tile.ensureStrictlyPositive(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 creating 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 * discarted. */ 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); if (!parent.overlaps) { TreeNode existing = parent.firstChildren(); while (existing != null) { if (child.intersects(existing) && !child.equals(existing)) { parent.overlaps = true; break; } existing = existing.nextSibling(); } } parent.addChild(child); } if (isFlat) { removeEmpty(); } /* * 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(); } } 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 inconditionnaly. 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; } /** * Returns the largest horizontal or vertical distance between this rectangle and the specified * one. Returns a negative number if the rectangles overlap. Diagonals are <strong>not</strong> * computed. * <p> * This method is not robust to integer arithmetic overflow. In such case, an * {@link AssertionError} is likely to be thrown if assertions are enabled. */ private int distance(final Rectangle rect) { int dx = rect.x - x; if (dx >= 0) { dx -= width; } else { dx += rect.width; dx = -dx; } int dy = rect.y - y; if (dy >= 0) { dy -= height; } else { dy += rect.height; dy = -dy; } final int distance = Math.max(dx, dy); assert (intersects(rect) ? (dx < 0 && dy < 0) : (distance >= 0)) : distance; return distance; } /** * Makes sure that this node and all its children do not contains overlapping tiles. If at * least one overlapping is found, then the nodes are reorganized in non-overlapping sub-nodes. * Algorithm overview: * <p> * <ol> * <li>For the current {@linkplain #children}, keeps the first node and remove every nodes * that overlap with a previous one (except special cases described above). The removed * nodes are stored in a temporary list.</li> * <li>The nodes selected in the previous step are groupped in a new {@link GridNode}, * which will be the first {@linkplain #children} of this tile.</li> * <li>Repeat the process with the nodes that were removed in the first step. Each new * group is added as a new {@linkplain #children} in this node.</li> * </ol> * <p> * <b>special case:</b> if an overlapping is found but the two nodes have identical bounds, * then they are considered as if they did not overlap. This exception exists because this * trivial overlap is easy to detect and to process by {@link RTree}. */ private void splitOverlappingChildren() { assert isLeaf() || !isEmpty() : this; // Requires that bounds has been computed. GridNode child = (GridNode) firstChildren(); while (child != null) { child.splitOverlappingChildren(); child = (GridNode) child.nextSibling(); } if (!overlaps) { return; } List<GridNode> toProcess = new LinkedList<GridNode>(); final List<GridNode> retained = new ArrayList<GridNode>(); child = (GridNode) firstChildren(); while (child != null) { toProcess.add(child); child = (GridNode) child.nextSibling(); } removeChildren(); // Necessary in order to give children to other nodes. int bestIndex=0, bestDistance=0; /* * The loop below is for processing a group of nodes. A "group of nodes" is either * the initial children list (on the first iteration), or the nodes that have not be * retained in a previous run of this loop. In the later case, those remaining nodes * need to be examined again and again until they are classified in some group. */ while (!toProcess.isEmpty()) { final List<GridNode> removed = new LinkedList<GridNode>(); GridNode added = toProcess.remove(0); retained.add(added); /* * The loop below is for moving every non-overlapping nodes to the "retained" list, * begining with the first node that we retained unconditionnaly in the above line * (we need to start with one node in order to select non-overlapping nodes...) */ ListIterator<GridNode> it; while ((it = toProcess.listIterator()).hasNext()) { GridNode best = null; /* * The loop below is for removing every nodes that overlap with the "added" node, * and select only one node (the "best" one) in the non-overlapping nodes. We * select the closest tile (relative to the "added" one) rather than the first * one because the retention order is significant. */ do { final GridNode candidate = it.next(); final int distance = added.distance(candidate); if (distance < 0) { /* * Found an overlapping tile. Accept inconditionnaly the tile if its bounds * is exactly equals to the "added" tile (this is the special case described * in the method javadoc above). Otherwise remove it from the toProcess list * and search for an other tile. */ if (added.equals(candidate)) { retained.add(candidate); } else { removed.add(candidate); } it.remove(); } else if (best == null || distance < bestDistance) { /* * The tile do not overlaps. Retain it only if it is the closest one * to the "added" tile. Otherwise left it in the toProcess list for * consideration in a future iteration. */ bestDistance = distance; bestIndex = it.previousIndex(); best = candidate; // Note: if the distance is 0 we could break the loop as an optimization // (since we can't get anything better), but we don't because we still // need to remove the overlapping tiles that may be present in toProcess. } } while (it.hasNext()); /* * If we found no non-overlapping tile, we are done. The toProcess list should be * empty now (it will be tested in an assert statement after the loop). Otherwise * move the best tile from the "toProcess" to the "retained" list. */ if (best == null) { break; } if (toProcess.remove(bestIndex) != best) { throw new AssertionError(bestIndex); } retained.add(best); added = best; } assert toProcess.isEmpty() : toProcess; assert Collections.disjoint(retained, removed); final GridNode[] sorted = retained.toArray(new GridNode[retained.size()]); retained.clear(); Arrays.sort(sorted, PRE_PROCESSING); child = new GridNode(this); assert child.isLeaf(); for (TreeNode newChild : sorted) { child.addChild(newChild); } addChild(child); toProcess = removed; } overlaps = false; } /** * Remove empty nodes. */ private void removeEmpty() { GridNode child = (GridNode) firstChildren(); while (child != null) { final GridNode next = (GridNode) child.nextSibling(); child.removeEmpty(); // May lost its nextSibling(). child = next; } if (tile == null && isLeaf() && !isRoot()) { remove(); } } /** * 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(); } dense = isDense(this, this); } /** * 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 garanteed // to be equals 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 this node has more than one tile and some of them overlaps. */ @Override public boolean hasOverlaps() { return overlaps; } /** * 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. * * @param nodes The nodes for which to prepend a tree. * @return The nodes with a tree prepend before them. */ private static GridNode[] prependTree(GridNode[] nodes) { final Dimension largest = new Dimension(); final Rectangle bounds = new Rectangle(-1,-1); for (final GridNode node : nodes) { bounds.add(node); if (node.width > largest.width) largest.width = node.width; if (node.height > largest.height) largest.height = node.height; } if (!bounds.isEmpty()) { /* * Asks for node that can contain at least 2×2 tiles, otherwise creating * those nodes would consume memory without significant performance gain. */ largest.width *= 2; largest.height *= 2; final int[][] divisors = MosaicBuilder.suggestedNumTiles(bounds, largest, 16, false); final int[] sx = divisors[0]; final int[] sy = divisors[1]; final Rectangle part = new Rectangle(); final List<GridNode> list = new ArrayList<GridNode>(); for (int i=0; i<sx.length; i++) { final int nx = sx[i]; final int ny = sy[i]; part.y = bounds.y; part.width = bounds.width / nx; part.height = bounds.height / ny; for (int y=0; y<ny; y++) { part.x = bounds.x; for (int x=0; x<nx; x++) { list.add(new GridNode(part)); part.x += part.width; } part.y += part.height; } } final int size = list.size(); final GridNode[] old = nodes; nodes = list.toArray(new GridNode[size + old.length]); System.arraycopy(old, 0, nodes, size, old.length); } return nodes; } /** * Returns {@code true} if the rectangles in the given collection fill completly the given * ROI with no empty space. * * @todo This method is not yet correctly implemented. For now we performs a naive check * which is suffisient for common {@link TileLayout}. We may need to revisit this * method in a future version. */ private static boolean isDense(final Rectangle roi, final Iterable<? extends Rectangle> regions) { Rectangle bounds = null; for (final Rectangle rect : regions) { final Rectangle inter = roi.intersection(rect); if (bounds == null) { bounds = inter; } else { bounds.add(inter); // See java.awt.Rectangle javadoc for empty rectangle handling. } } return bounds == null || bounds.equals(roi); } /** * Returns {@code true} if this node fills completly the given ROI with no empty space. * * @todo This method is not yet correctly implemented. For now we performs a naive check * which is suffisient for common {@link TileLayout}. We may need to revisit this * method in a future version. * * @todo Not yet used, but should be in a future version. See the TODO notice in {@link RTree}. */ public boolean isDense(final Rectangle roi) { assert contains(roi); return dense; } }