/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.*;
import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.io.IOException;
import org.geotools.coverage.grid.ImageGeometry;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.util.logging.Logging;
/**
* Creates a collection of {@link Tile tiles} from their <cite>grid to CRS</cite> affine transform.
* When the {@linkplain Rectangle rectangle} that describe the destination region is known for every
* tiles, {@linkplain Tile#Tile(ImageReader,Object,int,Rectangle,Dimension) tile constructor} can be
* invoked directly. But in some cases the destination region is not known directly. Instead we have
* a set of {@linkplain java.awt.image.BufferedImage buffered images} with a (0,0) location for each
* of them, and different <cite>grid to CRS</cite> affine transforms. This {@code RegionCalculator}
* class infers the destination regions automatically from the set of affine transforms.
*
* @since 2.5
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*/
final class RegionCalculator {
/**
* Small number for floating point comparisons.
*/
private static final double EPS = 1E-10;
/**
* The location of the final bounding box (the one including every tiles).
* Tiles will be translated as needed in order to fit this location.
*/
private final int xLocation, yLocation;
/**
* Tiles for which we should compute the bounding box only when we have them all.
* Their bounding box (region) will need to be adjusted for the affine transform.
*/
private final Map<AffineTransform,Tile> tiles;
/**
* Creates an initially empty tile collection with the location set to (0,0).
*/
public RegionCalculator() {
this(null);
}
/**
* Creates an initially empty tile collection with the given location.
*
* @param location The location, or {@code null} for (0,0).
*/
public RegionCalculator(final Point location) {
if (location != null) {
xLocation = location.x;
yLocation = location.y;
} else {
xLocation = yLocation = 0;
}
// We really need an IdentityHashMap, not an ordinary HashMap, because we will
// put many AffineTransforms that are equal in the sense of Object.equals but
// we still want to associate them to different Tile instances.
tiles = new IdentityHashMap<AffineTransform,Tile>();
}
/**
* Returns the location of the tile collections to be created. The location is usually (0,0)
* which match the {@linkplain java.awt.image.BufferedImage buffered image} location, but
* it doesn't have to.
*/
public Point getLocation() {
return new Point(xLocation, yLocation);
}
/**
* Adds a tile to the collection of tiles to process.
*
* @param tile The tile to add.
* @return {@code true} if the tile has been successfully added, or {@code false}
* if the tile doesn't need to be processed by this class.
*/
public boolean add(final Tile tile) {
final AffineTransform gridToCRS;
synchronized (tile) {
gridToCRS = tile.getPendingGridToCRS(true);
}
if (gridToCRS == null) {
return false;
}
if (tiles.put(gridToCRS, tile) != null) {
throw new AssertionError(); // Should never happen.
}
return true;
}
/**
* Returns the tiles. Keys are grid geometry (containing image bounds and <cite>grid to
* coordinate reference system</cite> transforms) and values are the tiles. This method
* usually returns a singleton map, but more entries may be present if this method was
* not able to build a single pyramid using all provided tiles.
* <p>
* <strong>Invoking this method flush the collection</strong>. On return, this instance
* is in the same state as if {@link #clear} has been invoked. This is because current
* implementation modify its workspace directly for efficiency.
*/
public Map<ImageGeometry,Tile[]> tiles() {
final Map<ImageGeometry,Tile[]> results = new HashMap<ImageGeometry,Tile[]>(4);
for (final Map<AffineTransform,Dimension> tilesAT : computePyramidLevels(tiles.keySet())) {
/*
* Picks an affine transform to be used as the reference one. We need the finest one.
* If more than one have the finest resolution, the exact choice does not matter much.
* But we will save a little bit of CPU if we pickup the one that will lead to a (0,0)
* translation at the end of this method.
*/
AffineTransform reference = null;
double xMin = Double.POSITIVE_INFINITY;
double xLead = Double.POSITIVE_INFINITY; // Minimum on the first row only.
double yMin = Double.POSITIVE_INFINITY;
double scale = Double.POSITIVE_INFINITY;
for (final AffineTransform tr : tilesAT.keySet()) {
final double s = XAffineTransform.getScale(tr);
double y = tr.getTranslateY(); if (tr.getScaleY() < 0 || tr.getShearY() < 0) y = -y;
double x = tr.getTranslateX(); if (tr.getScaleX() < 0 || tr.getShearX() < 0) x = -x;
if (!(Math.abs(s - scale) <= EPS)) {
if (!(s < scale)) continue; // '!' is for catching NaN.
scale = s; // Found a smaller scale.
yMin = y;
xMin = x;
} else { // Found a transform with the same scale.
if (x < xMin) xMin = x;
if (!(Math.abs(y - yMin) <= EPS)) {
if (!(y < yMin)) continue;
yMin = y; // Found a smaller y.
} else if (!(x < xLead)) continue;
}
xLead = x;
reference = tr;
}
/*
* If there is missing tiles at the begining of the first row, then the x location
* of the first tile is greater than the "true" minimum. We will need to adjust.
*/
if (reference == null) {
continue;
}
xLead -= xMin;
if (xLead > EPS) {
final double[] matrix = new double[6];
reference.getMatrix(matrix);
matrix[4] -= xLead;
reference = new AffineTransform(matrix);
} else {
reference = new AffineTransform(reference); // Protects from upcomming changes.
}
/*
* Transforms the image bounding box from its own space to the reference space. If
* 'computePyramidLevels' did its job correctly, the transform should contains only
* a scale and translation - no shear (we don't put assertions because of rounding
* errors). In such particular case, transforming a Rectangle2D is accurate. We
* round (we do not clip as in the default Rectangle implementation) because we
* really expect integer results.
*/
final AffineTransform toGrid;
try {
toGrid = reference.createInverse();
} catch (NoninvertibleTransformException e) {
throw new IllegalStateException(e);
}
int index = 0;
Rectangle groupBounds = null;
final Rectangle2D.Double envelope = new Rectangle2D.Double();
final Tile[] tilesArray = new Tile[tilesAT.size()];
for (final Map.Entry<AffineTransform,Dimension> entry : tilesAT.entrySet()) {
final AffineTransform tr = entry.getKey();
Tile tile = tiles.remove(tr); // Should never be null.
tr.preConcatenate(toGrid);
/*
* Computes the transformed bounds. If we fail to obtain it, there is probably
* something wrong with the tile (typically a wrong filename) but this is not
* fatal to this method. In such case, we will transform only the location instead
* of the full box, which sometime imply a lost of accuracy but not always. Note
* that the user is likely to obtains the same exception if the MosaicImageReader
* attempts to read the same tile (but as long as it doesn't, it may work).
*/
Rectangle bounds;
synchronized (tile) {
tile.setSubsampling(entry.getValue());
try {
bounds = tile.getRegion();
} catch (IOException exception) {
bounds = null;
Logging.unexpectedException(RegionCalculator.class, "tiles", exception);
}
if (bounds != null) {
XAffineTransform.transform(tr, bounds, envelope);
bounds.x = (int) Math.round(envelope.x);
bounds.y = (int) Math.round(envelope.y);
bounds.width = (int) Math.round(envelope.width);
bounds.height = (int) Math.round(envelope.height);
} else {
final Point location = tile.getLocation();
tr.transform(location, location);
bounds = new Rectangle(location.x, location.y, 0, 0);
}
tile.setAbsoluteRegion(bounds);
}
if (groupBounds == null) {
groupBounds = bounds;
} else {
groupBounds.add(bounds);
}
tilesArray[index++] = tile;
}
tilesAT.clear(); // Lets GC do its work.
/*
* Translates the tiles in such a way that the upper-left corner has the coordinates
* specified by (xLocation, yLocation). Adjusts the tile affine transform concequently.
* After this block, tiles having the same subsampling will share the same immutable
* affine transform instance.
*/
if (groupBounds != null) {
final int dx = xLocation - groupBounds.x;
final int dy = yLocation - groupBounds.y;
if (dx != 0 || dy != 0) {
reference.translate(-dx, -dy);
groupBounds.translate(dx, dy);
}
final ImageGeometry geometry = new ImageGeometry(groupBounds, reference);
reference = geometry.getGridToCRS(); // Fetchs the immutable instance.
final Map<Dimension,TranslatedTransform> pool =
new HashMap<Dimension,TranslatedTransform>();
for (final Tile tile : tilesArray) {
final Dimension subsampling = tile.getSubsampling();
TranslatedTransform translated = pool.get(subsampling);
if (translated == null) {
translated = new TranslatedTransform(subsampling, reference, dx, dy);
pool.put(subsampling, translated);
}
translated.applyTo(tile);
}
results.put(geometry, tilesArray);
}
}
return results;
}
/**
* Sorts affine transform by increasing X scales in absolute value.
* For {@link #computePyramidLevels} internal working only.
*/
private static final Comparator<AffineTransform> X_COMPARATOR = new Comparator<AffineTransform>() {
public int compare(final AffineTransform tr1, final AffineTransform tr2) {
return Double.compare(XAffineTransform.getScaleX0(tr1), XAffineTransform.getScaleX0(tr2));
}
};
/**
* Sorts affine transform by increasing Y scales in absolute value.
* For {@link #computePyramidLevels} internal working only.
*/
private static final Comparator<AffineTransform> Y_COMPARATOR = new Comparator<AffineTransform>() {
public int compare(final AffineTransform tr1, final AffineTransform tr2) {
return Double.compare(XAffineTransform.getScaleY0(tr1), XAffineTransform.getScaleY0(tr2));
}
};
/**
* From a set of arbitrary affine transforms, computes pyramid levels that can be given to
* {@link Tile} constructors. This method tries to locate the affine transform with finest
* resolution. This is typically (but not always, depending on rotation or axis flip) the
* transform with smallest {@linkplain AffineTransform#getScaleX scale X} and {@linkplain
* AffineTransform#getScaleY scale Y} coefficients in absolute value. This transform is
* given a dimension of (1,1) and stored in an {@linkplain IdentityHashMap identity hash
* map}. Other transforms are stored in the same map with their dimension relative to the
* first one, or discarded if the scale ratio is not an integer. In the later case, the
* transforms that were discarded from the first pass will be put in a new map to be added
* as the second element in the returned list. A new pass is run, discarded transforms from
* the second pass are put in the third element of the list, <cite>etc</cite>.
*
* @param gridToCRS The <cite>grid to CRS</cite> affine transforms computed from the
* image to use in a pyramid. The collection and the transform elements are not
* modified by this method (they may be modified by the caller however).
* @return A subset of the given transforms with their relative resolution. This method
* typically returns one map, but more could be returned if the scale ratio is
* not an integer for every transforms.
*/
private static List<Map<AffineTransform,Dimension>> computePyramidLevels(
final Collection<AffineTransform> gridToCRS)
{
final List<Map<AffineTransform,Dimension>> results =
new ArrayList<Map<AffineTransform,Dimension>>(2);
/*
* First, computes the pyramid levels along the X axis. Transforms that we were unable
* to classify will be discarded from the first run and put in a subsequent run.
*/
AffineTransform[] transforms = gridToCRS.toArray(new AffineTransform[gridToCRS.size()]);
Arrays.sort(transforms, X_COMPARATOR);
int length = transforms.length;
while (length != 0) {
final Map<AffineTransform,Dimension> result =
new IdentityHashMap<AffineTransform,Dimension>();
if (length <= (length = computePyramidLevels(transforms, length, result, false))) {
throw new AssertionError(length); // Should always be decreasing.
}
results.add(result);
}
/*
* Next, computes the pyramid levels along the Y axis. If we fail to compute the
* pyramid level for some AffineTransform, they will be removed from the map. If
* a map became empty because of that, the whole map will be removed.
*/
final Iterator<Map<AffineTransform,Dimension>> iterator = results.iterator();
while (iterator.hasNext()) {
final Map<AffineTransform,Dimension> result = iterator.next();
length = result.size();
transforms = result.keySet().toArray(transforms);
Arrays.sort(transforms, 0, length, Y_COMPARATOR);
length = computePyramidLevels(transforms, length, result, true);
while (--length >= 0) {
if (result.remove(transforms[length]) == null) {
throw new AssertionError(length);
}
}
if (result.isEmpty()) {
iterator.remove();
}
}
return results;
}
/**
* Computes the pyramid level for the given affine transforms along the X or Y axis, and
* stores the result in the given map.
*
* @param gridToCRS The AffineTransform to analyse. This array <strong>must</strong> be
* sorted along the dimension specified by {@code term}.
* @param length The number of valid entries in the {@code gridToCRS} array.
* @param result An initially empty map in which to store the results.
* @param isY {@code false} for analyzing the X axis, or {@code true} for the Y axis.
* @return The number of entries remaining in {@code gridToCRS}.
*/
private static int computePyramidLevels(final AffineTransform[] gridToCRS, final int length,
final Map<AffineTransform,Dimension> result, final boolean isY)
{
int processing = 0; // Index of the AffineTransform under process.
int remaining = 0; // Count of AffineTransforms that this method did not processed.
AffineTransform base;
double scale, shear;
boolean scaleIsNull, shearIsNull;
do {
if (processing >= length) {
return remaining;
}
base = gridToCRS[processing++];
if (isY) {
scale = base.getScaleY();
shear = base.getShearY();
} else {
scale = base.getScaleX();
shear = base.getShearX();
}
scaleIsNull = Math.abs(scale) < EPS;
shearIsNull = Math.abs(shear) < EPS;
} while (scaleIsNull && shearIsNull && redo(result.remove(base)));
if (isY) {
// If we get a NullPointerException here, it would be a bug in the algorithm.
result.get(base).height = 1;
} else {
assert result.isEmpty() : result;
result.put(base, new Dimension(1,0));
}
/*
* From this point, consider 'base', 'scale', 'shear', 'scaleIsNull', 'shearIsNull'
* as final. They describe the AffineTransform with finest resolution along one axis
* (X or Y), not necessarly both.
*/
while (processing < length) {
final AffineTransform candidate = gridToCRS[processing++];
final double scale2, shear2;
if (isY) {
scale2 = candidate.getScaleY();
shear2 = candidate.getShearY();
} else {
scale2 = candidate.getScaleX();
shear2 = candidate.getShearX();
}
final int level;
if (scaleIsNull) {
if (!(Math.abs(scale2) < EPS)) {
// Expected a null scale but was not.
gridToCRS[remaining++] = candidate;
continue;
}
level = level(shear2 / shear);
} else {
level = level(scale2 / scale);
if (shearIsNull ? !(Math.abs(shear2) < EPS) : (level(shear2 / shear) != level)) {
// Expected (a null shear) : (the same pyramid level), but was not.
gridToCRS[remaining++] = candidate;
continue;
}
}
if (level == 0) {
// Not a pyramid level (the ratio is not an integer).
gridToCRS[remaining++] = candidate;
continue;
}
/*
* Stores the pyramid level either as the width or as the height, depending on the
* 'isY' value. The map is assumed initially empty for the X values, and containing
* every required entries for the Y values.
*/
if (isY) {
// If we get a NullPointerException here, it would be a bug in the algorithm.
result.get(candidate).height = level;
} else {
if (result.put(candidate, new Dimension(level,0)) != null) {
throw new AssertionError(candidate); // Should never happen.
}
}
}
Arrays.fill(gridToCRS, remaining, length, null);
return remaining;
}
/**
* Computes the pyramid level from the ratio between two affine transform coefficients.
* If the ratio has been computed from {@code entry2.scaleX / entry1.scaleX}, then a
* return value of:
* <p>
* <ul>
* <li>1 means that both entries are at the same level.</li>
* <li>2 means that the second entry has pixels twice as large as first entry.</li>
* <li>3 means that the second entry has pixels three time larger than first entry.</li>
* <li><cite>etc...</cite></li>
* <li>A negative number means that the second entry has pixels smaller than first entry.</li>
* <li>0 means that the ratio between entries is not an integer number.</li>
* </ul>
*
* @param ratio The ratio between affine transform coefficients.
* @return The pixel size (actually subsampling) relative to the smallest pixel, or 0 if it
* can't be computed. If the ratio is between 0 and 1, then this method returns a
* negative number.
*/
private static int level(double ratio) {
if (ratio > 0 && ratio < Double.POSITIVE_INFINITY) {
// The 0.75 threshold could be anything between 0.5 and 1. We
// take a middle value for being safe regarding rounding errors.
final boolean inverse = (ratio < 0.75);
if (inverse) {
ratio = 1 / ratio;
}
final double integer = Math.rint(ratio);
if (integer < Integer.MAX_VALUE && Math.abs(ratio - integer) < EPS) {
// Found an integer ratio. Inverse the sign (just
// as a matter of convention) if smaller than 1.
int level = (int) integer;
if (inverse) {
level = -level;
}
return level;
}
}
return 0;
}
/**
* A hack for a {@code while} loop.
*/
private static boolean redo(final Dimension size) {
return true;
}
/**
* Returns a string representation of the tiles contained in this object. Since this method is
* for debugging purpose, only the first tiles may be formatted in order to avoid consumming to
* much space in the debugger.
*/
@Override
public String toString() {
final List<Tile> tiles = new ArrayList<Tile>(this.tiles.values());
Collections.sort(tiles);
return Tile.toString(tiles, 400);
}
}