/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-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.Map;
import java.util.List;
import java.util.Arrays;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.IOException;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.util.collection.FrequencySortedSet;
import static org.apache.sis.util.collection.Containers.hashMapCapacity;
/**
* A mosaic organized in the GDAL way, where each overview is contained in the same file.
* The scale factor between the base level and overviews may not be an integer. Fractional
* scale values are usually not legal, but happen in practice with GDAL mosaics.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.15
*
* @since 3.15
* @module
*/
final class GDALTileManager extends TileManager implements Comparator<Rectangle> {
/**
* For cross-version compatibility during serialization.
*/
private static final long serialVersionUID = 7452795743008991530L;
/**
* A threshold value for considering a mosaic layout as a GDAL layout.
* This is the minimal value required for the following quantity:
*
* <var>image size</var> / <var>maximal subsampling</var>.
*/
private static final int THRESHOLD = 8;
/**
* Sorts the tiles by decreasing subsampling value.
*/
private static final Comparator<Tile> BY_SUBSAMPLING = new Comparator<Tile>() {
@Override public int compare(final Tile tile1, final Tile tile2) {
final Dimension s1 = tile1.getSubsampling();
final Dimension s2 = tile2.getSubsampling();
return Long.signum(s2.width * (long) s2.height - s1.width * (long) s1.height);
}
};
/**
* The tiles in each geographic area. For any value of <var>i</var>, the array {@code tiles[i]}
* contains tiles in the same geographic area sorted by decreasing values of subsampling.
*/
private final Tile[][] tilesByRegion;
/**
* The regions, in "absolute" coordinates, of each array of tiles. For each index <var>i</var>,
* {@code regions[i]} is the union of the absolute regions of every tiles in {@code tiles[i]}.
*/
private final Rectangle[] tileRegions;
/**
* The region enclosing all tiles. This is the union of all rectangles in the
* {@link #tileRegions} array.
*/
private final Rectangle region;
/**
* {@code false} if the regions are sorted by <var>x</var> values,
* or {@code true} if they are sorted by <var>y</var> values.
*/
private final boolean sortedByY;
/**
* The tile dimensions. Will be computed only when first needed.
*/
private transient Dimension tileSize;
/**
* Creates a new tile manager for the given tiles.
*
* @param tiles The tiles, including overviews.
* @throws IOException if an I/O operation was required and failed.
* @throws IllegalArgumentException if this class can not handle the given tiles.
*/
protected GDALTileManager(final Tile[] tiles) throws IOException, IllegalArgumentException {
/*
* Find the highest subsampling values, and multiply them by 2. The intend is to
* compare the tile regions with a tolerance of hx pixels in width, or hy pixels
* in height, either as smaller or larger regions.
*/
int hx=1, hy=1;
for (final Tile tile : tiles) {
final Dimension s = tile.getSubsampling();
if (s.width > hx) hx = s.width;
if (s.height > hy) hy = s.height;
}
hx <<= 1;
hy <<= 1;
/*
* Create a list of tiles by region whith the region coordinates in units of highest
* subsampling. Note that we really want to perform the computation on (width,height)
* rather than (xmax,ymax) because the later cause variations of 1 pixel in rectangle
* sizes compared to the expected values.
*/
long sumWidth = 0, sumHeight = 0;
final Map<Rectangle,List<Tile>> byRegions = new HashMap<>();
for (final Tile tile : tiles) {
final Rectangle tileRegion = tile.getAbsoluteRegion();
sumWidth += tileRegion.width;
sumHeight += tileRegion.height;
tileRegion.x = divide(tileRegion.x, hx, false);
tileRegion.y = divide(tileRegion.y, hy, false);
tileRegion.width = divide(tileRegion.width, hx, true); // See above comment.
tileRegion.height = divide(tileRegion.height, hy, true);
if (tileRegion.width < THRESHOLD || tileRegion.height < THRESHOLD) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedImageSize));
}
List<Tile> list = byRegions.get(tileRegion);
if (list == null) {
list = new ArrayList<>();
byRegions.put(tileRegion, list);
}
list.add(tile);
}
/*
* Get the array of tiles for each region and sort them by decreasing subsampling.
*/
int i = 0;
final int numRegions = byRegions.size();
region = new Rectangle(-1, -1);
tileRegions = new Rectangle[numRegions];
tilesByRegion = new Tile[numRegions][];
for (final List<Tile> list : byRegions.values()) {
final int n = list.size();
if (n < 2) {
/*
* We require every region to contain at least one overview,
* otherwise the mosaic geometry may not be a GDAL one.
*/
throw new IllegalArgumentException(Errors.format(Errors.Keys.IncompatibleGridGeometry));
}
final Tile[] overviews = list.toArray(new Tile[n]);
Arrays.sort(overviews, BY_SUBSAMPLING);
final Rectangle tileRegion = new Rectangle(-1, -1);
for (final Tile overview : overviews) {
tileRegion.add(overview.getAbsoluteRegion());
}
tilesByRegion[i] = overviews;
tileRegions[i++] = tileRegion;
region.add(tileRegion);
}
/*
* Computes whatever sorting by x or by y ordinates is better, then sort the regions.
*/
sortedByY = (tiles.length * (long)region.height / sumHeight >=
tiles.length * (long)region.width / sumWidth);
final Map<Rectangle,Tile[]> byAbsoluteRegion = new IdentityHashMap<>(hashMapCapacity(numRegions));
for (i=0; i<numRegions; i++) {
byAbsoluteRegion.put(tileRegions[i], tilesByRegion[i]);
}
Arrays.sort(tileRegions, this);
for (i=0; i<numRegions; i++) {
if ((tilesByRegion[i] = byAbsoluteRegion.get(tileRegions[i])) == null) {
throw new AssertionError();
}
assert tileRegions[i].contains(tilesByRegion[i][0].getAbsoluteRegion());
}
}
/**
* Computes n/d with rounding toward positive or negative infinity.
*
* @param ceil {@code false} for rounding toward negative infinity, or
* {@code true} for rounding toward positive infinity.
*/
private static int divide(int n, final int d, final boolean ceil) {
if (ceil) if (n >= 0) n += (d-1);
else if (n < 0) n -= (d-1);
return n / d;
}
/**
* Compares two rectangle for order. The rectangles are ordered by either their <var>x</var>
* or <var>y</var> ordinates. The ordinate used is determined by the constructor.
*/
@Override
public int compare(final Rectangle r1, final Rectangle r2) {
return Long.signum(compare(r1, r2.x, r2.y));
}
/**
* Compares one rectangle with the (x,y) location of an other rectangle for order.
*/
private long compare(final Rectangle r1, final int x2, final int y2) {
int p1, p2;
if (sortedByY) {
p1 = r1.y;
p2 = y2;
} else {
p1 = r1.x;
p2 = x2;
}
if (p1 == p2) {
if (sortedByY) {
p1 = r1.x;
p2 = x2;
} else {
p1 = r1.y;
p2 = y2;
}
}
return (long) p1 - (long) p2;
}
/**
* Returns the region enclosing all tiles. This method returns a direct reference
* to the internal object; <strong>do not modify</strong>.
*/
@Override
final Rectangle getRegion() {
return region;
}
/**
* Returns the tiles dimension, to be returned by {@link MosaicImageReader#getTileWidth(int)}
* and similar methods. The current implementation returns the most frequent size of base tiles.
* <p>
* This method returns a direct reference to the internal object; <strong>do not modify</strong>.
*/
@Override
final synchronized Dimension getTileSize() {
if (tileSize == null) {
final FrequencySortedSet<Dimension> sizes = new FrequencySortedSet<>(true);
for (final Rectangle region : tileRegions) {
sizes.add(region.getSize());
}
tileSize = sizes.first();
}
return tileSize;
}
/**
* Returns {@code true} if there is more than one tile.
*/
@Override
final boolean isImageTiled() {
return tilesByRegion.length >= 2;
}
/**
* Copies every tiles from the given source array to the given flat target array.
*
* @param source The source array.
* @param target The target array, or {@code null}.
* @return The total number of tiles in the source array.
*/
private static int getTiles(final Tile[][] source, final Tile[] target) {
int n = 0;
for (final Tile[] tiles : source) {
if (target != null) {
System.arraycopy(tiles, 0, target, n, tiles.length);
}
n += tiles.length;
}
return n;
}
/**
* Returns every tiles in a flat list, in no particular order.
*/
@Override
public Collection<Tile> getTiles() {
final Tile[] all = new Tile[getTiles(tilesByRegion, null)];
getTiles(tilesByRegion, all);
return Arrays.asList(all);
}
/**
* Returns every tiles that intersect the given region.
*
* @param region The region of interest (shall not be {@code null}).
* @param subsampling On input, the minimal subsampling. On output, the effective subsampling.
* @param subsamplingChangeAllowed {@code true} if this method is allowed to modify subsampling.
* @return The tiles that intercept the given region. May be empty but never {@code null}.
*/
@Override
public Collection<Tile> getTiles(final Rectangle region, final Dimension subsampling,
final boolean subsamplingChangeAllowed) throws IOException
{
/*
* Get the tile arrays in the regions intersecting the requested region.
* This loop takes advantage of the regions ordering for stopping as soon as possible.
*/
final int xmax = region.x + region.width;
final int ymax = region.y + region.height;
int intersectCount = 0;
Tile[][] intersect = new Tile[Math.min(tilesByRegion.length, 4)][];
for (int i=0; i<tileRegions.length; i++) {
final Rectangle tr = tileRegions[i];
if (compare(tr, xmax, ymax) > 0) {
break; // There is no way the following tiles can intersect.
}
if (region.intersects(tr)) {
if (intersectCount == intersect.length) {
intersect = Arrays.copyOf(intersect, intersectCount << 1);
}
intersect[intersectCount++] = tilesByRegion[i];
}
}
final int[] startAt = new int[intersectCount]; // Initialized to 0.
/*
* For each array of tiles, search for the first tile having enough resolution.
* The resolution effectively used will be stored in the 'newResolution' object.
*/
Dimension newSubsampling = subsampling;
final List<Tile> result = new ArrayList<>();
for (int i=0; i<intersectCount; i++) {
final Tile[] tiles = intersect[i];
for (int j=startAt[i]; j<tiles.length; j++) {
final Tile tile = tiles[j];
final Dimension floor = tile.getSubsamplingFloor(newSubsampling);
if (floor == null) {
/*
* The tile at index j does not have enough resolution.
* Search for an other tile at finer resolution.
*/
continue;
}
if (floor != newSubsampling) {
/*
* The tile does not have the requested resolution. If we are not allowed
* to change that resolution, then we have to search for an other tile.
*/
if (!subsamplingChangeAllowed) {
continue;
}
/*
* If we are allowed to change the resolution, change it. But if the new
* resolution is not the same than the resolution computed for the previous
* tiles, we need to recompute everything we the new (finer) resolution.
*/
final boolean restart = (newSubsampling != subsampling);
newSubsampling = floor;
if (restart) {
result.clear();
startAt[i] = j;
i = -1;
break; // Restart the outer loop.
}
}
/*
* Add the tile that we just found and examine the next array. We test again for
* intersection because some overviews may cover a smaller region than the one
* stored in the tileRegions array.
*/
if (tile.getAbsoluteRegion().intersects(region)) {
result.add(tile);
}
startAt[i] = j+1;
break;
}
}
if (newSubsampling != subsampling) {
subsampling.setSize(newSubsampling);
}
removeOverlaps(result, region);
return result;
}
/**
* Removes the overlaps in the given list of tiles, if any. This is a convenience method
* to be invoked by {@link #getTiles(Rectangle, Dimension, boolean)} implementations.
* <p>
* This method should be invoked only for very small list, since its execution time
* is quadratic to the list size.
*
* @param tiles The list in which to remove overlaps.
* @param region The requested region.
* @throws IOException If it was necessary to fetch an image dimension from its
* {@linkplain Tile#getImageReader reader} and this operation failed.
*/
private static void removeOverlaps(final List<Tile> tiles, final Rectangle region) throws IOException {
int count = tiles.size();
final Rectangle[] intersect = new Rectangle[count];
for (int i=0; i<count; i++) {
intersect[i] = region.intersection(tiles.get(i).getAbsoluteRegion());
}
for (int i=0; i<count; i++) {
final Rectangle rc = intersect[i];
for (int j=count; --j>=0;) {
if (i != j && rc.contains(intersect[j])) {
System.arraycopy(intersect, j+1, intersect, j, --count - j);
tiles.remove(j);
if (j < i) i--;
}
}
}
}
/**
* Returns {@code true} if at least one tile having the given subsampling or a finer
* one intersects the given region.
*
* @param region The region of interest (shall not be {@code null}).
* @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.
*/
@Override
public boolean intersects(final Rectangle region, final Dimension subsampling) {
final int xmax = region.x + region.width;
final int ymax = region.y + region.height;
for (int i=0; i<tileRegions.length; i++) {
final Rectangle tr = tileRegions[i];
if (compare(tr, xmax, ymax) > 0) {
break; // There is no way the following tiles can intersect.
}
if (region.intersects(tr)) {
final Tile[] array = tilesByRegion[i];
for (int j=array.length; --j>=0;) {
final Dimension s = array[j].getSubsampling();
if (s.width <= subsampling.width && s.height <= subsampling.height) {
return true;
}
}
}
}
return false;
}
}