/* * 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.Set; import java.util.Map; import java.util.Arrays; import java.util.HashMap; import java.util.Collection; import java.util.NoSuchElementException; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.io.PrintWriter; import java.io.IOException; import java.io.Serializable; import java.util.Collections; import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import org.geotools.util.FrequencySortedSet; import org.geotools.coverage.grid.ImageGeometry; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * A collection of {@link Tile} objects to be given to {@link MosaicImageReader}. This base * class does not assume that the tiles are arranged in any particular order (especially grids). * But subclasses can make such assumption for better performances. * * @since 2.5 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux */ public abstract class TileManager implements Serializable { /** * For cross-version compatibility during serialization. */ private static final long serialVersionUID = -7645850962821189968L; /** * The grid geometry, including the "<cite>grid to real world</cite>" transform. * This is provided by {@link TileManagerFactory} when this information is available. */ ImageGeometry geometry; /** * All image providers used as an unmodifiable set. Computed when first needed. */ transient Set<ImageReaderSpi> providers; /** * Creates a tile manager. */ protected TileManager() { } /** * Sets the {@linkplain Tile#getGridTocRS grid to CRS} transform for every tiles. A copy of * the supplied affine transform is {@linkplain AffineTransform#scale scaled} according the * {@linkplain Tile#getSubsampling subsampling} of each tile. Tiles having the same * subsampling will share the same immutable instance of affine transform. * <p> * The <cite>grid to CRS</cite> transform is not necessary for proper working of {@linkplain * MosaicImageReader mosaic image reader}, but is provided as a convenience for users. * <p> * This method can be invoked only once. * * @param gridToCRS The "grid to CRS" transform. * @throws IllegalStateException if a transform was already assigned to at least one tile. * @throws IOException If an I/O operation was required and failed. */ public synchronized void setGridToCRS(final AffineTransform gridToCRS) throws IllegalStateException, IOException { if (geometry != null) { throw new IllegalStateException(); } final Map<Dimension,AffineTransform> shared = new HashMap<Dimension,AffineTransform>(); AffineTransform at = new XAffineTransform(gridToCRS); shared.put(new Dimension(1,1), at); geometry = new ImageGeometry(getRegion(), at); for (final Tile tile : getInternalTiles()) { final Dimension subsampling = tile.getSubsampling(); at = shared.get(subsampling); if (at == null) { at = new AffineTransform(gridToCRS); at.scale(subsampling.width, subsampling.height); at = new XAffineTransform(at); shared.put(subsampling, at); } tile.setGridToCRS(at); } } /** * Returns the grid geometry, including the "<cite>grid to real world</cite>" transform. * This information is typically available only when {@linkplain AffineTransform affine * transform} were explicitly given to {@linkplain Tile#Tile(ImageReaderSpi,Object,int, * Dimension,AffineTransform) tile constructor}. * * @return The grid geometry, or {@code null} if this information is not available. * @throws IOException If an I/O operation was required and failed. * * @see Tile#getGridToCRS */ public synchronized ImageGeometry getGridGeometry() throws IOException { if (geometry == null) { /* * The gridToCRS transform is the same one than the one of the tile having origin at * (0,0) and subsampling of (1,1). So we search for exactly this tile and currently * accept no other one. In a future version we could accept an other tile (but which * one?) and translate the affine transform... But the result could be wrong if the * gridToCRS transform is not computed by RegionCalculator. Only the particular tile * searched by current implementation should be okay in all cases. */ for (final Tile tile : getInternalTiles()) { final Dimension subsampling = tile.getSubsampling(); if (subsampling.width != 1 || subsampling.height != 1) { continue; } final Point origin = tile.getLocation(); if (origin.x != 0 || origin.y != 0) { continue; } final AffineTransform gridToCRS = tile.getGridToCRS(); if (gridToCRS == null) { continue; } geometry = new ImageGeometry(getRegion(), gridToCRS); break; } } return geometry; } /** * Returns the region enclosing all tiles. Subclasses will override this method with * a better implementation. * * @return The region. <strong>Do not modify</strong> since it may be a direct reference to * internal structures. * @throws IOException If it was necessary to fetch an image dimension from its * {@linkplain Tile#getImageReader reader} and this operation failed. */ Rectangle getRegion() throws IOException { return getGridGeometry().getGridRange(); } /** * Returns the tiles dimension. Subclasses will override this method with a better * implementation. * * @return The tiles dimension. <strong>Do not modify</strong> since it may be a direct * reference to internal structures. * @throws IOException If it was necessary to fetch an image dimension from its * {@linkplain Tile#getImageReader reader} and this operation failed. */ Dimension getTileSize() throws IOException { return getRegion().getSize(); } /** * Returns {@code true} if there is more than one tile. The default implementation returns * {@code true} in all cases. * * @return {@code true} if the image is tiled. * @throws IOException If an I/O operation was required and failed. */ boolean isImageTiled() throws IOException { return true; } /** * Returns all image reader providers used by the tiles. The set will typically contains * only one element, but more are allowed. In the later case, the entries in the set are * sorted from the most frequently used provider to the less frequently used. * * @return The image reader providers. * @throws IOException If an I/O operation was required and failed. * * @see MosaicImageReader#getTileReaderSpis */ public synchronized Set<ImageReaderSpi> getImageReaderSpis() throws IOException { if (providers == null) { final FrequencySortedSet<ImageReaderSpi> providers = new FrequencySortedSet<ImageReaderSpi>(4, true); final Collection<Tile> tiles = getInternalTiles(); int[] frequencies = null; if (tiles instanceof FrequencySortedSet) { frequencies = ((FrequencySortedSet<Tile>) tiles).frequencies(); } int i = 0; for (final Tile tile : tiles) { final int n = (frequencies != null) ? frequencies[i++] : 1; providers.add(tile.getImageReaderSpi(), n); } this.providers = Collections.unmodifiableSet(providers); } return providers; } /** * Creates a tile with a {@linkplain Tile#getRegion region} big enough for containing * {@linkplain #getTiles every tiles}. The created tile has a {@linkplain Tile#getSubsampling * subsampling} of (1,1). This is sometime useful for creating a "virtual" image representing * the assembled mosaic as a whole. * * @param provider * The image reader provider to be given to the created tile, or {@code null} for * inferring it automatically. In the later case the provider is inferred from the * input suffix if any (e.g. the {@code ".png"} extension in a filename), or * failing that the most frequently used provider is selected. * @param input * The input to be given to the created tile. It doesn't need to be an existing * {@linkplain java.io.File file} or URI since this method will not attempt to * read it. * @param imageIndex * The image index to be given to the created tile (usually 0). * @return A global tile big enough for containing every tiles in this manager. * @throws NoSuchElementException * If this manager do not contains at least one tile. * @throws IOException * If an I/O operation was required and failed. */ public Tile createGlobalTile(ImageReaderSpi provider, final Object input, final int imageIndex) throws NoSuchElementException, IOException { if (provider == null) { // Following line may throw the NoSuchElementException documented in javadoc. provider = getImageReaderSpis().iterator().next(); ImageReaderSpi inferred = Tile.getImageReaderSpi(input); if (inferred != null && inferred != provider) { final Collection<String> f1 = Arrays.asList(provider.getFormatNames()); final Collection<String> f2 = Arrays.asList(inferred.getFormatNames()); if (!f1.containsAll(f2)) { provider = inferred; } } } final Tile tile; final ImageGeometry geometry = getGridGeometry(); if (geometry == null) { tile = new LargeTile(provider, input, imageIndex, getRegion()); } else { tile = new LargeTile(provider, input, imageIndex, geometry.getGridRange()); tile.setGridToCRS(geometry.getGridToCRS()); } return tile; } /** * Returns a reference to the tiles used internally by the tile manager. The returned collection * must contains only direct references to the tiles hold internally, not instances created on * the fly (as {@link GridTileManager} can do). This is because we want to update the state of * those tiles in a persistent way if this method is invoked by {@link #setGridToCRS}. * <p> * Callers of this method should not rely on the {@linkplain Tile#getInput tile input} and * should not attempt to read the tiles, since the inputs can be non-existant files or patterns * (again the case of {@link GridTileManager}). This method is not public for that reason. * <p> * The default implementation returns {@link #getTiles}. * * @return The internal tiles. If the returned collection is an instance of * {@link FrequencySortedSet}, then the frequencies will be honored * in methods where it matter like {@link #getImageReaderSpis}. * @throws IOException If an I/O operation was required and failed. */ Collection<Tile> getInternalTiles() throws IOException { return getTiles(); } /** * Returns all tiles. * * @return The tiles. * @throws IOException If an I/O operation was required and failed. */ public abstract Collection<Tile> getTiles() throws IOException; /** * Returns every tiles that intersect the given region. * * @param region * The region of interest (shall not be {@code null}). * @param subsampling * On input, the number of source columns and rows to advance for each pixel. On * output, the effective values to use. Those values may be different only if * {@code subsamplingChangeAllowed} is {@code true}. * @param subsamplingChangeAllowed * If {@code true}, this method is allowed to replace {@code subsampling} by the * highest subsampling that overviews can handle, not greater than the given * subsampling. * @return The tiles that intercept the given region. May be empty but never {@code null}. * @throws IOException If it was necessary to fetch an image dimension from its * {@linkplain Tile#getImageReader reader} and this operation failed. */ public abstract Collection<Tile> getTiles(Rectangle region, Dimension subsampling, boolean subsamplingChangeAllowed) throws IOException; /** * Returns {@code true} if at least one tile having the given subsampling or a finer * one intersects the given region. The default implementation returns {@code true} if * <code>{@linkplain #getTiles(Rectangle,Dimension,boolean) getTiles}(region, subsampling, false)</code> * returns a non-empty set. Subclasses are encouraged to provide a more efficient implementation. * * @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. * @throws IOException If it was necessary to fetch an image dimension from its * {@linkplain Tile#getImageReader reader} and this operation failed. */ public boolean intersects(Rectangle region, Dimension subsampling) throws IOException { return !getTiles(region, subsampling, false).isEmpty(); } /** * Checks for file existence and image size of every tiles and reports any error found. * * @param out Where to report errors ({@code null} for default, which is the * {@linkplain System#out standard output stream}). */ public void printErrors(PrintWriter out) { if (out == null) { out = new PrintWriter(System.out, true); } final Collection<Tile> tiles; try { tiles = getTiles(); } catch (IOException e) { e.printStackTrace(out); return; } for (final Tile tile : tiles) { final int imageIndex = tile.getImageIndex(); ImageReader reader = null; String message = null; try { final Rectangle region = tile.getRegion(); reader = tile.getImageReader(); final int width = reader.getWidth (imageIndex); final int height = reader.getHeight(imageIndex); if (width != region.width || height != region.height) { message = Errors.format(ErrorKeys.UNEXPECTED_IMAGE_SIZE); } Tile.dispose(reader); reader = null; } catch (IOException exception) { message = exception.toString(); } catch (RuntimeException exception) { message = exception.toString(); } if (message != null) { out.println(tile); out.print(" "); out.println(message); } // In case an exception was thrown before Tile.dispose(reader). if (reader != null) { reader.dispose(); } } } /** * Returns a string representation of this tile manager. The default implementation * formats the first tiles in a table. Subclasses may format the tiles in a tree * instead. Note that in both cases the result may be a quite long string. * * @return A string representation. */ @Override public String toString() { final Collection<Tile> tiles; try { tiles = getTiles(); } catch (IOException e) { return e.toString(); } /* * If each lines are 100 characters long, then limiting the formatting to 10000 tiles * will limit memory consumption to approximatively 1 Mb. */ return Tile.toString(tiles, 10000); } }