/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008-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.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.URL; import java.net.URI; import java.net.URISyntaxException; import java.net.MalformedURLException; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.collection.IntegerList; import org.geotoolkit.util.collection.FrequencySortedSet; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.internal.storage.IOUtilities; import org.geotoolkit.resources.Errors; import static org.geotoolkit.image.io.mosaic.Tile.LOGGER; /** * A level of overview in a {@linkplain GridTileManager gridded tile manager}. Instances of this * class can not be created or modified by public methods. * <p> * <b>Note:</b> This class as a {@link #compareTo} method which is inconsistent with * {@link #equals}. * * {@section Thread-safety} * The methods in this class as grouped in two categories: constructor methods and query methods. * Constructor methods should be invoked in only one thread, and {@link #createLinkedList} shall * be the last method invoked. Query methods are safe for invocation in any thread provided that * {@code OverviewLevel} is not modified anymore after construction. * * @author Martin Desruisseaux (Geomatys) * @version 3.15 * * @since 2.5 * @module */ final class OverviewLevel implements Comparable<OverviewLevel>, Serializable { /** * For cross-version compatibility. */ private static final long serialVersionUID = -1441934881339348L; /** * The input types for which we will try to find a pattern. */ private static final Set<Class<?>> INPUT_TYPES = new HashSet<>(8); static { INPUT_TYPES.add(String.class); INPUT_TYPES.add(File .class); INPUT_TYPES.add(Path.class); INPUT_TYPES.add(URL .class); INPUT_TYPES.add(URI .class); } /** * A level with finer (smaller) subsampling value than this level, or {@code null} if none. * Will be set by {@link #createLinkedList}. */ private OverviewLevel finer; /** * The overview level of this {@code OverviewLevel}. 0 is finest subsampling. Must match the * element index in the sorted {@code GridTileManager.levels} array. */ private int ordinal; /** * The number of tiles along <var>x</var> and <var>y</var> axis. * Will be computed by {@link #createLinkedList}. */ private int nx, ny; /** * Subsampling of every tiles at this level. */ private final int xSubsampling, ySubsampling; /** * The location of the tile closest to origin, positive. They are <cite>relative</cite> * coordinates as used in public {@link Tile} API (i.e. those coordinates are <em>not</em> * pre-multiplied by {@link #xSubsampling} and {@link #ySubsampling}). */ private final int xOffset, yOffset; /** * Size of every tiles at this level. They are <cite>relative</cite> size as used in * public {@link Tile} API (i.e. those coordinates are <em>not</em> pre-multiplied by * {@link #xSubsampling} and {@link #ySubsampling}). */ private final int dx, dy; /** * The region of every tiles in this level. The {@linkplain Rectangle#x x} and * {@linkplain Rectangle#y y} coordinates are the upper-left corner of the (0,0) * tile. The {@linkplain Rectangle#width width} and {@linkplain Rectangle#height height} * are big enough for including every tiles. * <p> * They are <cite>relative</cite> coordinates as used in public {@link Tile} API * (i.e. those coordinates are <em>not</em> pre-multiplied by {@link #xSubsampling} * and {@link #ySubsampling}). */ private final Rectangle mosaic; /** * On construction, the list of tiles {@linkplain #add added} in this level in no particular * order. After {@linkplain #createLinkedList processing}, the tiles that need to be retained * because they can not be created on the fly from the {@linkplain #patterns}, or {@code null} * if none. */ private List<Tile> tiles; /** * The tiles to use as a pattern for creating tiles on the fly, or {@code null} if none. * If non-null, then the array length is typically 1. If greater than one, then the * {@linkplain #usePattern} field needs to be non-null in order to specify which pattern * is used. */ private Tile[] patterns; /** * If there is more than one pattern, the index of pattern to use. Also used for signaling * holes in the mosaic if there is any. */ private IntegerList patternUsed; // // Synchronization policy // ---------------------- // All above fields shall be set by constructor methods and are not allowed to change once // the construction is completed. Access to those fields are generally not synchronized. // // All fields below this point are used by query methods, mostly for caching purpose. // Access to those fields must be synchronized. // /** * A sample tile which can be used as a pattern. This is just one amont many possible tiles. */ private transient Tile sample; /** * Index of last pattern used. Used in order to avoid reinitializing * the {@linkplain #formatter} more often than needed. */ private transient int lastPattern; /** * The formatter used for parsing and creating filename. */ private transient FilenameFormatter formatter; /** * Creates a new level using the given pattern. The {@link #createLinkedList} method should * be invoked directly after this constructor, without prior calls to {@link #add}. * * @param pattern The tile to use as a pattern. * @param region The region encompassing every tiles at this level, in relative coordinates. * @throws IOException if an error occurred while reading tile information. */ OverviewLevel(final Tile pattern, Rectangle region) throws IOException { final Dimension subsampling = pattern.getSubsampling(); final Rectangle tile = pattern.getRegion(); mosaic = region = new Rectangle(region); int x = tile.x % (dx = tile.width); int y = tile.y % (dy = tile.height); if (x < 0) x += dx; if (y < 0) y += dy; xOffset = x; yOffset = y; xSubsampling = subsampling.width; ySubsampling = subsampling.height; patterns = new Tile[] { pattern }; } /** * Creates a new level with only one initial tile. More tiles will need to be added by * invoking {@link #add}, and {@link #createLinkedList} must be invoked when every tiles * are there. * <p> * The tile given to this constructor is particular in that it will defines the origin * and size of grid cells. It must be a typical tile, not a tile in the last column or * last row which may be smaller than typical tiles. * * @param tile The tile to wrap. * @param subsampling The tile subsampling, provided as an explicit argument only * in order to avoid creating a temporary {@link Dimension} object again. * @throws IOException if an error occurred while reading tile information. */ OverviewLevel(final Tile tile, final Dimension subsampling) throws IOException { mosaic = tile.getRegion(); int x = mosaic.x % (dx = mosaic.width); int y = mosaic.y % (dy = mosaic.height); if (x < 0) x += dx; if (y < 0) y += dy; xOffset = x; yOffset = y; assert subsampling.equals(tile.getSubsampling()) : subsampling; xSubsampling = subsampling.width; ySubsampling = subsampling.height; tiles = new ArrayList<>(); tiles.add(tile); } /** * Adds a tile to the list of tiles in this level, provided that they are aligned on the same * grid. * * {@section Thread safety} * This method is <strong>not</strong> synchronized because it is invoked only by * {@link GridTileManager} constructor soon after {@code OverviewLevel} creation. * * @param tile The tile to add. * @param subsampling The tile subsampling, provided as an explicit argument only * in order to avoid creating a temporary {@link Dimension} object again. * @throws IOException if an I/O operation was required and failed. * @throws IllegalArgumentException if the tiles are not aligned on the same grid. */ final void add(final Tile tile, final Dimension subsampling) throws IOException, IllegalArgumentException { assert subsampling.equals(tile.getSubsampling()) : subsampling; assert subsampling.width == xSubsampling && subsampling.height == ySubsampling : subsampling; final Rectangle toAdd = tile.getRegion(); if (toAdd.width > dx || toAdd.height > dy) { throw new IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedImageSize)); } int ox = toAdd.x % dx; int oy = toAdd.y % dy; if (ox < 0) ox += dx; if (oy < 0) oy += dy; if ((ox -= xOffset) < 0 || (ox + toAdd.width) > dx || (oy -= yOffset) < 0 || (oy + toAdd.height) > dy) { throw new IllegalArgumentException(Errors.format(Errors.Keys.NotAGrid)); } mosaic.add(toAdd); tiles.add(tile); } /** * Once every tiles have been {@linkplain #add added} to this grid level, links this overview * to a finer overview level. This method also looks for a pattern in the tile name (in this * overview level only) in order to reduce the memory consumption. * * {@section Synchronization} * This method is synchronized as a matter of principle, because it uses the transient * {@link #formatter} field. This is also the last method invoked during the construction * by {@link GridTileManager}. Consequently the synchronization provides a useful memory * barrier. * * @param ordinal The overview level of this {@code OverviewLevel}. 0 is finest subsampling. * @param finer A level with finer (smaller) subsampling value than this level, or {@code null}. * @throws MalformedURLException if an error occurred while creating the URL for the tile. * @throws IOException If an error occurred while reading a tile size. */ final synchronized void createLinkedList(final int ordinal, final OverviewLevel finer) throws IOException { assert (this.ordinal == 0) && (this.finer == null); this.ordinal = ordinal; this.finer = finer; assert getFinerLevel() == finer; // For running the assertions inside getFinerLevel(). nx = (mosaic.width + (dx - 1)) / dx; // Round toward positive infinity. ny = (mosaic.height + (dy - 1)) / dy; assert (tiles == null) != (patterns == null); // Exactly one of those should be non-null. if (patterns != null) { /* * If this overview level has been created from a pattern, then we are done. */ return; } /* * Searches for the most common tuple of ImageReaderSpi, imageIndex, input pattern. The * rectangle below is named "size" because the (x,y) location is not representative. * The tiles that we failed to modelize by a pattern will be stored under the null key. */ formatter = new FilenameFormatter(); final Rectangle size = new Rectangle(xOffset, yOffset, dx, dy); final Map<Tile,List<Tile>> models = new HashMap<>(); for (final Tile tile : tiles) { final String input = inputPattern(tile); final Tile model = (input != null) ? new Tile(tile, input, size) : null; List<Tile> similar = models.get(model); if (similar == null) { similar = new ArrayList<>(); models.put(model, similar); } similar.add(tile); } /* * If there is at least one tile that can not be processed, keep them in an array. * The array length is exactly (nx*ny) but contains only the elements that should * not be computed on the fly (other elements are null). Note that if the number * of elements to be computed on the fly is less than some arbitrary threshold, * it is not worth to compute them on the fly so we move them to the tiles list. */ tiles = models.remove(null); for (final Iterator<List<Tile>> it = models.values().iterator(); it.hasNext();) { final List<Tile> similar = it.next(); if (similar.size() < 4) { if (tiles == null) { tiles = similar; } else { tiles.addAll(similar); } it.remove(); } } if (tiles != null) { tiles = UnmodifiableArrayList.wrap(toArray(tiles)); } /* * If there is no recognized pattern, clears the unused fields and finish immediately * this method, so we skip the construction of "pattern used" list (which may be large). * Note that we clears the formatter unconditionally because the last pattern guessed * in the 'inputPattern' method may be wrong. */ formatter = null; if (models.isEmpty()) { return; } /* * Sets the pattern index. Index in the 'tile' array are numbered from 0 (like usual), * but values in the 'patternUsed' list are numbered from 1 because we reserve the 0 * value for non-existent tiles. */ patterns = new Tile[models.size()]; patternUsed = new IntegerList(nx*ny, patterns.length, true); int index = 0; for (final Map.Entry<Tile,List<Tile>> entry : models.entrySet()) { patterns[index++] = entry.getKey(); for (final Tile tile : entry.getValue()) { final Point pt = getIndex2D(tile); final int i = getIndex(pt.x, pt.y); final int p = patternUsed.getInt(i); if ((p != 0 && p != index) || (tiles != null && tiles.get(i) != null)) { throw duplicatedTile(pt); } patternUsed.setInt(i, index); } } /* * In the common case where there is only one pattern and no missing tiles, * clears the 'patternUsed' construct since we don't need it. */ if (patterns.length == 1) { for (int i=patternUsed.size(); --i >= 0;) { if (patternUsed.getInt(i) == 0) { if (tiles == null || tiles.get(i) == null) { // We have at least one hole, so we need to keep the list of them. return; } } } patternUsed = null; } } /** * Returns a pattern for the given tile. If no pattern can be found, returns {@code null}. * This method accepts only tile and input of specific types in order to be able to rebuild * later an exactly equivalent object from the pattern. * * @param tile The tile to inspect for a pattern in the input object. * @return The pattern, or {@code null} if none. */ private String inputPattern(final Tile tile) throws IOException { /* * Accepts only instance of Tile (not a subclass), otherwise we will not know how to create * the instance on the fly. Once we have verified that the class is Tile, we are allowed to * check the tile size using the 'isSizeEquals' shortcut. We accept only tiles that fill * completely the cell size, otherwise we can not recreate the tile from a pattern. */ if (tile.getClass() != Tile.class) { return null; } if (!tile.isSizeEquals(dx, dy)) { /* * If the tile size is not the expected one, check again using a more costly * computation which work for tiles in the last column and last row. */ final Point index = getIndex2D(tile); if (!tile.getRegion().equals(getCellBounds(index.x, index.y))) { return null; } } final Object input = tile.getInput(); final Class<?> type = input.getClass(); if (!INPUT_TYPES.contains(type)) { return null; } final Point index = getIndex2D(tile); String pattern = input.toString(); pattern = formatter.guessPattern(ordinal, index.x, index.y, pattern); if (pattern != null) { pattern = type.getSimpleName() + ':' + pattern; } return pattern; } /** * Formats an exception for a duplicated tile. * * @param pt The upper-left corner coordinate. * @return An exception formatted for a duplicated tile at the given coordinate. */ private static IllegalArgumentException duplicatedTile(final Point pt) { return new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedValuesForKey_1, "location=" + pt.x + ',' + pt.y)); } /** * Removes the tile at the given index. Current implementation can remove only tiles * created from a pattern. * * {@section Thread safety} * This method is <strong>not</strong> synchronized, because it is invoked * only by {@link MosaicBuilder} soon after {@code OverviewLevel} creation. */ final void removeTile(final int tileX, final int tileY) { final int i = getIndex(tileX, tileY); assert tiles == null || tiles.get(i) == null; if (patternUsed == null) { patternUsed = new IntegerList(nx*ny, patterns.length, true); patternUsed.fill(1); } patternUsed.setInt(i, 0); } /** * Expands the given tiles in a flat array. Tiles are stored by their index, with * <var>x</var> index varying faster. */ private Tile[] toArray(final Collection<Tile> tiles) { final Tile[] array = new Tile[nx * ny]; for (final Tile tile : tiles) { final Point pt = getIndex2D(tile); final int index = getIndex(pt.x, pt.y); if (array[index] != null && !tile.equals(array[index])) { throw duplicatedTile(pt); } array[index] = tile; } return array; } ///////////////////////////////////////////////////////////////////////////////// //// //// //// End of construction methods. The remainding is for querying only. //// //// None of the methods below should modify the OverviewLevel state, //// //// except for transient fields. //// //// //// ///////////////////////////////////////////////////////////////////////////////// /** * Converts the search rectangle from <cite>absolute space</cite> to * <cite>tile index space</cite>. Index can not be negative neither * greater than ({@linkplain #nx},@linkplain #ny}). The (xmin,ymin) * index are inclusive while the (xmax,ymax) index are exclusive. * * @param search The search region in absolute coordinate. This rectangle will not be modified. * @return The search region as tile index. */ private Rectangle toTileIndex(final Rectangle search) { final Rectangle index = new Rectangle(dx * xSubsampling, dy * ySubsampling); /* * Computes min values. */ int x = search.x - mosaic.x * xSubsampling; int y = search.y - mosaic.y * ySubsampling; if (x >= 0) index.x = x / index.width; // Otherwise lets (x,y) to its default value (0). if (y >= 0) index.y = y / index.height; /* * Computes max values, inclusive. Round upper tiles indices toward lower integers * because they are inclusive, then add 1 to make them exclusive. The result is not * allowed to be greater than (nx, ny). */ x += search.width - 1; y += search.height - 1; index.width = Math.min(nx, x / index.width + 1) - index.x; index.height = Math.min(ny, y / index.height + 1) - index.y; return index; } /** * Returns the index of the given tile. The tile in the upper-left corner has index (0,0). * * @param tile The tile for which to get the index. * @return The index in a two-dimensional grid. */ private Point getIndex2D(final Tile tile) { final Point location = tile.getLocation(); location.x -= mosaic.x; location.y -= mosaic.y; assert (location.x % dx == 0) && (location.y % dy == 0) : location; location.x /= dx; location.y /= dy; return location; } /** * Returns the flat index for the given 2D index. * * @param tileX,tileY The tile location, with (0,0) as the upper-left tile. * @return The corresponding index in a flat array. * @throws IndexOutOfBoundsException if the given index is out of bounds. */ private int getIndex(final int tileX, final int tileY) throws IndexOutOfBoundsException { if (tileX < 0 || tileX >= nx || tileY < 0 || tileY >= ny) { throw new IndexOutOfBoundsException(Errors.format(Errors.Keys.IndexOutOfBounds_1, "(" + tileX + ',' + tileY + ')')); } return tileY * nx + tileX; } /** * Returns a level finer than this level, or {@code null} if this level is already the finest * one. * * @return The next level toward finer ones, or {@code null} if none. */ public OverviewLevel getFinerLevel() { assert ((finer != null) ? (ordinal > 0) : (ordinal == 0)) : ordinal; assert (finer == null) || (compareTo(finer) >= 0 && finer.ordinal == ordinal-1) : finer; return finer; } /** * Returns the number of tiles at this level. * * @return The number of tiles. */ public int getNumTiles() { int count = 0; if (patterns != null) { count = nx * ny; if (patternUsed != null) { count -= patternUsed.occurrence(0); } } else if (tiles != null) { for (final Tile tile : tiles) { if (tile != null) { count++; } } } return count; } /** * Returns the number of tiles along the <var>x</var> axis. * * @return The number of tiles in a row. */ public final int getNumXTiles() { return nx; } /** * Returns the number of tiles along the <var>y</var> axis. * * @return The number of tiles in a column. */ public final int getNumYTiles() { return ny; } /** * If there is more than one tile, returns the tile size. Otherwise returns {@code null}. * This special condition on the number of tile exists for {@link GridTileManager} * implementation convenience. * * @return The tile size, or {@code null} if there is only one tile. */ public Dimension getTileSize() { if (mosaic.width > dx || mosaic.height > dy) { return new Dimension(dx, dy); } return null; } /** * Returns the tiles bounding box in <cite>absolute</cite> coordinates. This is * the bounding box that this level would have if its subsampling was 1. * * @return The region in absolute coordinates. */ public Rectangle getAbsoluteRegion() { return new Rectangle(xSubsampling * mosaic.x, ySubsampling * mosaic.y, xSubsampling * mosaic.width, ySubsampling * mosaic.height); } /** * Returns {@code true} if the given bounds (in absolute coordinates) matches exactly the * region of a tile or a group of tiles at this level. This method do not checks if the * tiles actually exist. * * @param bounds The bounds to test. * @return {@code true} if the given bounds matches tiles bounds. */ private boolean isAbsoluteTilesRegion(final Rectangle bounds) { final int width = dx * xSubsampling; // Tile width in "absolute" units. if (bounds.width % width == 0) { final int height = dy * ySubsampling; // Tile height in "absolute" units. if (bounds.height % height == 0) { return (bounds.x - xOffset*xSubsampling) % width == 0 && (bounds.y - yOffset*ySubsampling) % height == 0; } } return false; } /** * Returns a sample tile. The tile may be {@linkplain Tile#getLocation located} anywhere, * and the {@linkplain Tile#getInput tile input} may not be usable (it may be only a * pattern for creating input on the fly). */ public synchronized Tile getSampleTile() { if (sample == null) { if (patterns != null) { /* * Should never be empty and should never contains null elements (but we still do * a loop for paranoia). If we get an IndexOutOfBoundsException, then it would be * a bug in the createLinkedList(...) method. */ int i = 0; do { sample = patterns[i++]; } while (sample == null); } else { /* * Should never be null when patterns == null and never empty. It may contains * some null elements, but at least one element should be nun-null. If we get a * NullPointerException or an IndexOutOfBoundsException, then it would be a bug * in the createLinkedList(...) method. */ int i = 0; do { sample = tiles.get(i++); } while (sample == null); } } return sample; } /** * Returns the tile at the given index. * * @param x,y The tile location, with (0,0) as the upper-left tile. * @return The tile at the given location, or {@code null} if none. * @throws IndexOutOfBoundsException if the given index is out of bounds. * @throws MalformedURLException if an error occurred while creating the URL for the tile. */ final Tile getTile(final int tileX, final int tileY) throws IndexOutOfBoundsException, MalformedURLException { final int index = getIndex(tileX, tileY); /* * Checks for fully-created instance. Those instances are expected to exist if * some tile do not comply to a general pattern that this class can recognize. */ if (tiles != null) { final Tile tile = tiles.get(index); if (tile != null) { // If a tile is explicitly defined, it should not have a pattern. assert patternUsed == null || patternUsed.getInt(index) == 0 : index; return tile; } // Tests here because it would be an error to have null patterns when tiles == null, // so we are better to lets NullPointerException been thrown in such case so we can // debug. if (patterns == null) { return null; } } /* * The requested tile does not need to be handled in a special way, so now get the * pattern for this tile and generate the filename of the fly. Doing so avoid the * consumption of memory for the thousands of tiles we may have. */ int p = 0; if (patternUsed != null) { p = patternUsed.get(index); if (p == 0) { return null; } p--; } final Tile tile = patterns[p]; final String pattern = tile.getInput().toString(); final String filename; synchronized (this) { if (formatter == null) { formatter = new FilenameFormatter(); lastPattern = -1; } if (p != lastPattern) { formatter.applyPattern(pattern.substring(pattern.indexOf(':') + 1)); lastPattern = p; } filename = formatter.generateFilename(ordinal, tileX, tileY); } /* * We now have the filename to be given to the tile. Creates the appropriate object * (File, URL, URI or String) from it. */ final Object input; if (pattern.startsWith("File")) { input = new File(filename); } else if (pattern.startsWith("Path")) { input = Paths.get(filename); } else if (pattern.startsWith("URL")) { input = new URL(filename); } else if (pattern.startsWith("URI")) { try { input = new URI(IOUtilities.encodeURI(filename)); } catch (URISyntaxException cause) { // Rethrown as an IOException subclass. MalformedURLException e = new MalformedURLException(cause.getLocalizedMessage()); e.initCause(cause); throw e; } } else { input = filename; } assert isValidInput(input) : input; /* * Now creates the definitive tile. The tiles in the last * row or last column may be smaller than other tiles. */ return new Tile(tile, input, getCellBounds(tileX, tileY)); } private boolean isValidInput(Object input) { for (Class<?> inputType : INPUT_TYPES) { if (inputType.isAssignableFrom(input.getClass())) { return true; } } return false; } /** * Computes the bounds that the tile at the given index would have, in relative coordinates * (<strong>not</strong> premultiplied by {@link #xSubsampling} and {@link #ySubsampling}). * This method does not check if a special tile is defined for that index. */ private Rectangle getCellBounds(final int tileX, final int tileY) { final int x = tileX * dx; final int y = tileY * dy; return new Rectangle( mosaic.x + x, mosaic.y + y, Math.min(dx, mosaic.width - x), Math.min(dy, mosaic.height - y)); } /** * Adds all internal tiles to the given set, together with their frequency. * * @param addTo The collection where to add the internal tiles. */ final void getInternalTiles(final FrequencySortedSet<? super Tile> addTo) { int count = 0; if (tiles != null) { for (final Tile tile : tiles) { if (tile != null) { addTo.add(tile); count++; } } } if (patterns != null) { for (int p=0; p<patterns.length;) { final Tile tile = patterns[p++]; final int n = (patternUsed != null) ? patternUsed.occurrence(p) : nx*ny - count; addTo.add(tile, n); } } } /** * Adds to the given list every tiles that intersect the given region. This is * caller responsibility to ensure that this level uses the subsampling of interest. * * @param addTo The list where to add the tiles. * @param search The region of interest in absolute coordinates. * @param subsampling The subsampling to apply on the tiles to be read. * Used for cost calculation. * @param costLimit If reading the returned tiles would have a cost equals or higher * than the given number, stop the search and returns {@code null}. * @return The cost of reading the tiles, or {@code -1} if the cost limit has been reached * (in which case no tiles has been added to the list). * @throws IOException if an error occurred while creating the URL for the tiles. */ final long getTiles(final ArrayList<Tile> addTo, final Rectangle search, final Dimension subsampling, final long costLimit) throws IOException { final Rectangle atr = toTileIndex(search); final int minTileX = atr.x; // Inclusive final int minTileY = atr.y; final int maxTileX = atr.width + minTileX; // Exclusive final int maxTileY = atr.height + minTileY; /* * Recycles the rectangle created by toTileIndex. The "atr" name stands for "Absolute * Tile Region". Width and height will not change anymore. X and y will be set later. */ atr.width = dx * xSubsampling; atr.height = dy * ySubsampling; final int ox = mosaic.x * xSubsampling; final int oy = mosaic.y * ySubsampling; final int size = addTo.size(); if (size == 0) { final int n = (maxTileX - minTileX) * (maxTileY - minTileY); addTo.ensureCapacity(n); } /* * The expected number of tiles is (xmax-xmin)*(ymax-ymin). However we may get less tiles * if the iteration gets some null tiles. We may also get more tiles if we put tiles from * finer levels into the mix (as the loop below may do). */ long totalCost = 0; for (int tileY=minTileY; tileY<maxTileY; tileY++) { nextTile: for (int tileX=minTileX; tileX<maxTileX; tileX++) { final Tile tile = getTile(tileX, tileY); if (tile == null) { continue; } /* * We have found a tile to add to the list. Before doing so, computes the cost * of reading this tile and checks if reading a tile at a finer level would be * cheaper. */ final long cost = tile.countUnwantedPixelsFromAbsolute(search, subsampling); if (cost != 0) { totalCost += cost; if (totalCost >= costLimit) { /* * The new tile increases the cost above the limit. Forget the tiles found * so far and cancel the search. Note that it is theorically possible that * the search in finer levels (code below) finds cheaper tiles which would * have allowed us to stay below the cost limit. We could have enabled this * case by performing this check at the end of the loop rather than now. * However doing so implies that every levels are tested recursively down * to the finest level. We have more to gain by stopping this method early * instead. */ addTo.subList(size, addTo.size()).clear(); return -1; } atr.x = ox + atr.width * tileX; atr.y = oy + atr.height * tileY; // Following assertion is enforced only if the Tile is not a custom implementation. assert atr.contains(tile.getAbsoluteRegion()) || tile.getClass() != Tile.class : tile; OverviewLevel previous = this; while ((previous = previous.getFinerLevel()) != null) { if (!previous.isAbsoluteTilesRegion(atr)) { continue; } final Rectangle clipped = atr.intersection(search); final long c = previous.getTiles(addTo, clipped, subsampling, cost); if (c >= 0) { // Tiles at the finer level are cheaper than the current tiles. So keep // them (they have been added to the 'addTo' array) and discart 'tile'. totalCost += (c - cost); continue nextTile; } break; } } addTo.add(tile); } } assert (addTo.size() > size) == intersects(search); return totalCost; } /** * Returns {@code true} if at least one tile intersects the given region. * This method does not search recursively into finer levels. * * @param search The region (in absolute coordinates) where to search for tiles. * @return {@code true} if at least one tile intersects the given region. * @throws IOException if an error occurred while fetching a tile size. */ final boolean intersects(final Rectangle search) throws IOException { final Rectangle index = toTileIndex(search); final int xmin = index.x; final int ymin = index.y; final int xmax = index.width + xmin; final int ymax = index.height + ymin; for (int y=ymin; y<ymax; y++) { for (int x=xmin; x<xmax; x++) { final int i = getIndex(x, y); if (tiles != null) { final Tile tile = tiles.get(i); if (tile != null) { if (search.intersects(tile.getAbsoluteRegion())) { return true; } else { continue; } } // If there is an explicit list of tiles, we may have no pattern. In this // case we don't want to return 'true' on the 'patternUsed' check below. if (patterns == null) { continue; } } if (patternUsed == null || patternUsed.get(i) != 0) { return true; } } } return false; } /** * Returns {@code true} if this level or any finer level contains the given tile. This method * is static in order to prevent accidental usage of implicit {@code this}, which would be a * bug. At the different of other (non-static) methods, this one is recursive. * * @param tile The tile to check for inclusion. * @reutrn {@code true} if this manager contains the given tile. */ static boolean contains(OverviewLevel level, final Tile tile) { final Dimension subsampling = tile.getSubsampling(); while (level != null) { if (level.xSubsampling == subsampling.width && level.ySubsampling == subsampling.height) { final Point index = level.getIndex2D(tile); if (index.x >= 0 && index.x < level.nx && index.y >= 0 && index.y < level.ny) try { // Reminder: level.getTile(x,y) may returns null. return tile.equals(level.getTile(index.x, index.y)); } catch (MalformedURLException e) { // If we can't format the name, then it is different than the given tile // input otherwise the user wouldn't have been able to create that tile. Logging.recoverableException(LOGGER, OverviewLevel.class, "contains", e); } break; } level = level.getFinerLevel(); } return false; } /** * Compares subsamplings, sorting smallest areas first. If two subsamplings have the * same area, sorts by <var>xSubsampling</var> first then by <var>ySubsampling</var>. * <p> * The algorithm applied in this method must be identical to {@link #compareTo(OverviewLevel)}. */ public int compareTo(final Dimension subsampling) { int c = (xSubsampling * ySubsampling) - (subsampling.width * subsampling.height); if (c == 0) { c = xSubsampling - subsampling.width; if (c == 0) { c = ySubsampling - subsampling.height; } } return c; } /** * Compares subsamplings, sorting smallest areas first. If two subsamplings have the * same area, sorts by <var>xSubsampling</var> first then by <var>ySubsampling</var>. * <p> * The algorithm applied in this method must be identical to {@link #compareTo(Dimension)}. */ @Override public int compareTo(final OverviewLevel other) { int c = (xSubsampling * ySubsampling) - (other.xSubsampling * other.ySubsampling); if (c == 0) { c = xSubsampling - other.xSubsampling; if (c == 0) { c = ySubsampling - other.ySubsampling; } } return c; } /** * Compares this overview level with the given object for equality. * * @param other The other object to compare for equality. * @return {@code true} if the given object is equal to this overview level. */ @Override public boolean equals(final Object other) { if (other instanceof OverviewLevel) { final OverviewLevel that = (OverviewLevel) other; return ordinal == that.ordinal && dx == that.dx && dy == that.dy && nx == that.nx && ny == that.ny && xSubsampling == that.xSubsampling && ySubsampling == that.ySubsampling && xOffset == that.xOffset && yOffset == that.yOffset && Objects.equals(this.mosaic, that.mosaic) && Objects.equals(this.tiles, that.tiles) && Arrays .equals(this.patterns, that.patterns) && Objects.equals(this.patternUsed, that.patternUsed) && Objects.equals(this.finer, that.finer); } return false; } /** * Returns a hash code value for this overview level. */ @Override public int hashCode() { int code = ordinal + 31 * (xSubsampling + 31 * (ySubsampling + Arrays.hashCode(patterns))); if (finer != null) { code += 31 * finer.hashCode(); } return code; } /** * Returns a string representation for debugging purpose. */ @Override public String toString() { return getClass().getSimpleName() + '[' + ordinal + ", subsampling=(" + xSubsampling + ',' + ySubsampling + "), " + getNumTiles() + " tiles]"; } }