/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007-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.io.*; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.List; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import org.geotoolkit.factory.Hints; import org.geotoolkit.factory.Factory; import org.geotoolkit.nio.PathFilterVisitor; import org.geotoolkit.resources.Errors; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.NullArgumentException; import org.geotoolkit.coverage.grid.ImageGeometry; import org.geotoolkit.referencing.operation.matrix.XAffineTransform; import static org.geotoolkit.image.io.mosaic.Tile.LOGGER; /** * Creates {@link TileManager} instances from a collection or a directory of tiles. * * @author Martin Desruisseaux (Geomatys) * @version 3.18 * * @since 2.5 * @module */ public class TileManagerFactory extends Factory { /** * The default instance. */ public static final TileManagerFactory DEFAULT = new TileManagerFactory(EMPTY_HINTS); /** * Creates a new factory from the specified hints. * * @param hints Optional hints, or {@code null} if none. */ protected TileManagerFactory(final Hints hints) { // We have no usage for those hints at this time, but some may be added later. } /** * Creates tile managers from the specified object, which may be {@code null}. If non-null, the * object shall be an instance of {@code TileManager[]}, {@code TileManager}, {@code Tile[]}, * {@code Collection<Tile>}, {@link Path}. or {@link File}. * * @param tiles The tiles, or {@code null}. * @return The tile managers, or {@code null} if {@code tiles} was null. * @throws IllegalArgumentException if {@code tiles} is not an instance of a valid class, * or if it is an array or a collection containing null elements. * @throws IOException If an I/O operation was required and failed. * * @see MosaicImageReader.Spi#getInputTypes() */ public TileManager[] createFromObject(final Object tiles) throws IOException, IllegalArgumentException { final TileManager[] managers; if (tiles == null) { managers = null; } else if (tiles instanceof Path) { managers = create((Path) tiles); } else if (tiles instanceof File) { managers = create(((File) tiles).toPath()); } else if (tiles instanceof TileManager[]) { managers = ((TileManager[]) tiles).clone(); } else if (tiles instanceof TileManager) { managers = new TileManager[] {(TileManager) tiles}; } else if (tiles instanceof Tile[]) { managers = create((Tile[]) tiles); } else if (tiles instanceof Collection<?>) { @SuppressWarnings("unchecked") // create(Collection) will checks indirectly. final Collection<Tile> c = (Collection<Tile>) tiles; managers = create(c); } else { throw new IllegalArgumentException(Errors.format( Errors.Keys.IllegalArgumentClass_3, "tiles", tiles.getClass(), TileManager.class)); } if (managers != null) { for (int i=0; i<managers.length; i++) { if (managers[i] == null) { throw new NullArgumentException(Errors.format( Errors.Keys.NullArgument_1, "input[" + i + ']')); } } } return managers; } /** * Creates a tile manager from the given file or directory. * <p> * <ul> * <li>If the argument {@linkplain Files#isRegularFile(Path, LinkOption...)} is a file} having the {@code ".serialized"} * suffix, then this method deserializes the object in the given file and passes it to * {@link #createFromObject(Object)}.</li> * <li>If the given argument {@linkplain Files#isDirectory(Path, LinkOption...)} is a directory}, then this * method delegates to {@link #create(Path, PathMatcher, ImageReaderSpi)} which scan * all image files found in the directory.</li> * <li>Otherwise an {@link IOException} is thrown.</li> * </ul> * * @param file The serialized file or the directory to scan. * @return A tile manager created from the tiles in the given directory. * @throws IOException If the given file is not recognized, or an I/O operation failed. * * @since 3.15 */ public TileManager[] create(final Path file) throws IOException { if (Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { final String suffix = file.getFileName().toString(); if (!suffix.endsWith(".serialized")) { throw new IOException(Errors.format(Errors.Keys.UnknownFileSuffix_1, suffix)); } final Object manager; try (InputStream fin = Files.newInputStream(file); ObjectInputStream in = new ObjectInputStream(fin)) { try { manager = in.readObject(); } catch (ClassNotFoundException cause) { InvalidClassException ex = new InvalidClassException(cause.getLocalizedMessage()); ex.initCause(cause); throw ex; } } return setSourceFile(createFromObject(manager), file); } else if (Files.isDirectory(file, LinkOption.NOFOLLOW_LINKS)) { return create(file, null, null); } else { throw new IOException(Errors.format(Errors.Keys.NotADirectory_1, file)); } } /** * Creates tile managers from the files found in the given directory. This method scans * also the sub-directories recursively if the given file matcher accepts directories. * <p> * First, this method fetches the list of all filtered files in the directory and sub-directories. * Then this method invokes the {@link #listTiles(ImageReaderSpi, File[])} method for converting * the list of files into a list of {@link Tile} instances. Note that in current implementation, * this implies that each image file in the given directory shall have a * <a href="http://fr.wikipedia.org/wiki/World_file">World File</a> of the same name, * typically with the {@code ".tfw"} extension. Finally this method builds the tiles manager * using the {@link #create(Collection)} method. * <p> * If the file matcher is null, a default file matcher will be created from the given SPI. * If the given SPI is null, a SPI will be inferred automatically for each image files * found in the directory. * * @param directory The directory to scan. * @param matcher An optional file matcher, or {@code null} for a default matcher. * @param spi An optional image provider, or {@code null} for auto-detect. * @return A tile manager created from the tiles in the given directory. * @throws IOException If the given file is not a directory, or an I/O operation failed. * * @since 3.15 */ public TileManager[] create(final Path directory, PathMatcher matcher, final ImageReaderSpi spi) throws IOException { if (matcher == null) { matcher = new ImagePathMatcher(spi); } final List<Path> files = listFiles(directory, matcher); return setSourceFile(create(listTiles(spi, files.toArray(new Path[files.size()]))), directory); } /** * Invokes {@link TileManager#setSourceFile(Path)} for all managers in the given array. * Returns the given array for convenience. */ private static TileManager[] setSourceFile(final TileManager[] managers, final Path file) { for (final TileManager manager : managers) { manager.setSourceFile(file); } return managers; } /** * Gets the files in the given directory and all sub-directories. * * @param directory The directory to scan. * @param matcher An optional file matcher, or {@code null}. * @return The files. * * @since 3.15 */ private static List<Path> listFiles(final Path directory, final PathMatcher matcher) throws IOException { if (!Files.isDirectory(directory)) { throw new IOException(Errors.format(Errors.Keys.NotADirectory_1, directory)); } final PathFilterVisitor visitor = new PathFilterVisitor(matcher); Files.walkFileTree(directory, visitor); return visitor.getMatchedPaths(); } /** * Creates tile managers from the specified array of tiles. * This method usually returns a single tile manager, but more could be * returned if this factory has been unable to put every tiles in a single mosaic * (for example if the ratio between {@linkplain AffineTransform affine transform} given to * {@linkplain Tile#Tile(ImageReaderSpi,Object,int,Rectangle,AffineTransform) tile constructor} * would lead to fractional {@linkplain Tile#getSubsampling subsampling}). * * @param tiles The tiles to give to a tile manager. * @return A tile manager created from the given tiles. * @throws IOException If an I/O operation was required and failed. */ public TileManager[] create(final Tile... tiles) throws IOException { // The default called invokes Collection.toArray(), which will copy the array. return create(Arrays.asList(tiles)); } /** * Creates tile managers from the specified collection of tiles. * This method usually returns a single tile manager, but more could be * returned if this factory has been unable to put every tiles in a single mosaic * (for example if the ratio between {@linkplain AffineTransform affine transform} given to * {@linkplain Tile#Tile(ImageReaderSpi,Object,int,Rectangle,AffineTransform) tile constructor} * would lead to fractional {@linkplain Tile#getSubsampling subsampling}). * * @param tiles The tiles to give to a tile manager. * @return A tile manager created from the given tiles. * @throws IOException If an I/O operation was required and failed. */ public TileManager[] create(Collection<Tile> tiles) throws IOException { int count = 0; final TileManager[] managers; if (!hasPendingGridToCRS(tiles)) { /* * There is no tile having a "gridToCRS" transform pending RegionCalculator work. So we * can create (at the end of this method) a single TileManager using all those tiles. */ if (!tiles.isEmpty()) { count = 1; } managers = new TileManager[count]; } else { /* * At least one tile have a pending "gridToCRS" transform (actually we should have * more than one - typically all of them - otherwise the RegionCalculator work will * be useless). Computes their region now. Note that we could execute this block * unconditionally. The 'hasPendingGridToCRS' check we just for avoiding the cost * of creating RegionCalculator in the common case where it is not needed. So it is * not a big deal if 'hasPendingGridToCRS' conservatively returned 'true'. */ final Collection<Tile> remainings = new ArrayList<>(Math.min(16, tiles.size())); final RegionCalculator calculator = new RegionCalculator(); for (final Tile tile : tiles) { if (!calculator.add(tile)) { remainings.add(tile); } } if (!remainings.isEmpty()) { count = 1; } final Map<ImageGeometry,Tile[]> split = calculator.tiles(); managers = new TileManager[split.size() + count]; for (final Map.Entry<ImageGeometry,Tile[]> entry : split.entrySet()) { final TileManager manager = createGeneric(entry.getValue()); manager.setGridGeometry(entry.getKey()); managers[count++] = manager; } tiles = remainings; } /* * The collection now contains tiles that has not been processed by RegionCalculator, * because their 'gridToCRS' transform is flagged as already computed. Create a mosaic * for them, and use the affine transform having the finest resolution as the "global" * one. */ if (!tiles.isEmpty()) { final TileManager manager = createGeneric(tiles.toArray(new Tile[tiles.size()])); final Rectangle imageBounds = new Rectangle(-1, -1); AffineTransform gridToCRS = null; Dimension subsampling = null; double scale = Double.POSITIVE_INFINITY; for (final Tile tile : tiles) { imageBounds.add(tile.getAbsoluteRegion()); final AffineTransform candidate = tile.getGridToCRS(); if (candidate != null && !candidate.equals(gridToCRS)) { final double cs = XAffineTransform.getScale(candidate); if (cs < scale) { // Found a new tile at a finer resolution. scale = cs; gridToCRS = candidate; subsampling = tile.getSubsampling(); } else if (cs == scale) { // Inconsistent transform at the finest level. // Abandon the attempt to create a grid geometry. gridToCRS = null; break; } } } if (gridToCRS != null) { if (subsampling.width != 1 || subsampling.height != 1) { gridToCRS = new AffineTransform(gridToCRS); gridToCRS.scale(subsampling.width, subsampling.height); } manager.setGridGeometry(new ImageGeometry(imageBounds, gridToCRS)); } managers[0] = manager; } return managers; } /** * Returns {@code true} if at least one tile in the given collection has at "<cite>grid to * real world</cite>" transform waiting to be processed by {@link RegionCalculator}. It is * okay to conservatively returns {@code true} in situations where we would have got * {@code false} if synchronization was performed on every tiles. */ private static boolean hasPendingGridToCRS(final Collection<Tile> tiles) { for (final Tile tile : tiles) { if (tile.getPendingGridToCRS(false) != null) { return true; } } return false; } /** * Creates a single {@linkplain TileManager tile manager} from the given array * of tiles. This method is automatically invoked by {@code create} methods. * The tile array has already been cloned and can be stored directly by the * tile manager constructors. * <p> * Subclasses can override this method if they want to create other kinds of tile managers. * * @param tiles A copy of user-supplied tiles. * @return The tile manager for the given tiles. * @throws IOException If an I/O operation was required and failed. */ protected TileManager createGeneric(final Tile[] tiles) throws IOException { TileManager manager; try { manager = new GridTileManager(tiles); } catch (IllegalArgumentException e) { Logging.recoverableException(LOGGER, GridTileManager.class, "<init>", e); try { manager = new GDALTileManager(tiles); } catch (IllegalArgumentException e2) { // Failed to created the instance optimized for grid. // Fallback on the more generic instance using RTree. Logging.recoverableException(LOGGER, GDALTileManager.class, "<init>", e2); return new TreeTileManager(tiles); } } // Intentional side effect: use ComparedTileManager only if assertions are enabled. assert (manager = new ComparedTileManager(manager, new TreeTileManager(tiles))) != null; return manager; } /** * Returns a list of tiles constructed from the given array of files. Every file in the given * array must exist, be a valid image and have a valid <cite>World File</cite>, i.e. a file of * the same name in the same directory then the image file, but with an extension like * {@code ".tfw"} (for TIFF images) or {@code ".jgw"} (for JPEG images). * <p> * This method loads the World Files and fetches the image sizes immediately. The world file * applies to the first image in the file. If the file contains more than one image, then each * additional image is assumed to represent the same data than the first image at a different * resolution. * * {@section Customization} * If the files contain many images but those images are <strong>not</strong> overviews of * the first image at different resolution, then subclasses can use only a specific image * by overriding this method as below, where {@code imageIndex} is the index of the image * to use (typically 0): * * {@preformat java * public List<Tile> listTiles(final ImageReaderSpi provider, final Path... inputs) throws IOException { * final List<Tile> tiles = new ArrayList<Tile>(inputs.length); * for (final Path input : inputs) { * tiles.add(new Tile(provider, input, imageIndex)); * } * return tiles; * } * } * * This method is invoked by {@link #create(Path, PathMatcher, ImageReaderSpi)}, so it can be * overridden for controlling the tiles to be built when {@code TileManagerFactory} scans a * directory. * * @param provider The image reader provider to use. If {@code null}, the provider will be * inferred from each input. If it can't be inferred, then an exception is thrown. * @param inputs The image files. * @return The tiles constructed from the image files. * @throws IOException If no <cite>World File</cite> were found for a given image file, or * if an error occurred while reading a file. * * @see Tile#Tile(ImageReaderSpi, File, int) * * @since 3.18 */ public List<Tile> listTiles(final ImageReaderSpi provider, final Path... inputs) throws IOException { try (final TileReaderPool readers = new TileReaderPool()) { final Set<ImageReaderSpi> providers = new HashSet<>(); final List<Tile> tiles = new ArrayList<>(inputs.length); for (final Path input : inputs) { tiles.addAll(toTiles(provider, readers, providers, input)); } readers.dispose(); return tiles; } } private List<Tile> toTiles(ImageReaderSpi provider, TileReaderPool readers, Set<ImageReaderSpi> providers, Path input) throws IOException { // Creates the tile for the first image, which usually have the maximal resolution. // The Tile constructor will read the TFW file and infer a provider if the given // 'provider' argument is null. If this is a new provider, then we need to declare // it to the pool of image readers before to use it. final List<Tile> tiles = new ArrayList<>(); final Tile root = new Tile(provider, input, 0); if (providers.add(root.getImageReaderSpi())) { readers.setProviders(providers); } final AffineTransform scaledGridToCRS = new AffineTransform(); final AffineTransform gridToCRS = root.getPendingGridToCRS(false); final ImageReader reader = root.getImageReader(readers, true, true); final int numImages = reader.getNumImages(false); // Result may be -1. for (int index=0; index != numImages; index++) { // Intentional use of !=, not <. final int width, height; try { width = reader.getWidth(index); height = reader.getHeight(index); } catch (IndexOutOfBoundsException e) { // As explained in ImageReader javadoc, this approach is sometime // more efficient than invoking reader.getNumImages(true) first. break; } final Tile tile; if (index == 0) { tile = root; } else { final Rectangle region = root.getRegion(); scaledGridToCRS.setTransform(new AffineTransform(gridToCRS)); scaledGridToCRS.scale(region.width / (double) width, region.height / (double) height); tile = new Tile(root.getImageReaderSpi(), input, index, region, scaledGridToCRS); } tile.setSize(width, height); tiles.add(tile); } reader.dispose(); return tiles; } }