/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 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.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.geotools.util.Utilities; import org.geotools.util.IntegerList; import org.geotools.util.logging.Logging; import org.geotools.util.FrequencySortedSet; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.UnmodifiableArrayList; /** * 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}. * * @since 2.5 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux */ 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<Class<?>>(8); static { INPUT_TYPES.add(String.class); INPUT_TYPES.add(File .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; /** * 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 occured 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 occured 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<Tile>(); tiles.add(tile); } /** * Adds a tile to the list of tiles in this level, provided that they are aligned on the same * grid. * * @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(ErrorKeys.UNEXPECTED_IMAGE_SIZE)); } 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(ErrorKeys.NOT_A_GRID)); } mosaic.add(toAdd); tiles.add(tile); } /** * Once every tiles have been {@linkplain #add added} to this grid level, search for a pattern. * * @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 occured while creating the URL for the tile. */ final void createLinkedList(final int ordinal, final OverviewLevel finer) throws MalformedURLException { 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; } /* * Searchs 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<Tile,List<Tile>>(); 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<Tile>(); 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 unconditionnaly 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-existant 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.getInteger(i); if ((p != 0 && p != index) || (tiles != null && tiles.get(i) != null)) { throw duplicatedTile(pt); } patternUsed.setInteger(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.getInteger(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) { /* * 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 * completly the cell size, otherwise we can not recreate the tile from a pattern. */ if (!Tile.class.equals(tile.getClass()) || !tile.isSizeEquals(dx, dy)) { 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( ErrorKeys.DUPLICATED_VALUES_$1, "location=" + pt.x + ',' + pt.y)); } /** * Removes the tile at the given index. Current implementation can remove only tiles * created from a pattern. */ final void removeTile(final int x, final int y) { final int i = getIndex(x, y); assert tiles == null || tiles.get(i) == null; if (patternUsed == null) { patternUsed = new IntegerList(nx*ny, patterns.length, true); patternUsed.fill(1); } patternUsed.setInteger(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. //// //// //// ///////////////////////////////////////////////////////////////////////////////// /** * 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. We round (width,height) toward higher integer. x += search.width; y += search.height; index.width = Math.min(nx, (x + (index.width - 1)) / index.width) - index.x; index.height = Math.min(ny, (y + (index.height - 1)) / index.height) - 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 x,y 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 x, final int y) throws IndexOutOfBoundsException { if (x < 0 || x >= nx || y < 0 || y >= ny) { throw new IndexOutOfBoundsException(Errors.format( ErrorKeys.INDEX_OUT_OF_BOUNDS_$1, "(" + x + ',' + y + ')')); } return y * nx + x; } /** * 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.occurence(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 usuable (it may be only a * pattern for creating input on the fly). */ public 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 occured while creating the URL for the tile. */ final Tile getTile(int x, int y) throws IndexOutOfBoundsException, MalformedURLException { final int index = getIndex(x, y); /* * 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.getInteger(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(); if (formatter == null) { formatter = new FilenameFormatter(); lastPattern = -1; } if (p != lastPattern) { formatter.applyPattern(pattern.substring(pattern.indexOf(':') + 1)); lastPattern = p; } final String filename = formatter.generateFilename(ordinal, x, y); /* * 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("URL")) { input = new URL(filename); } else if (pattern.startsWith("URI")) try { input = new URI(filename); } catch (URISyntaxException cause) { // Rethrown as an IOException subclass. MalformedURLException e = new MalformedURLException(cause.getLocalizedMessage()); e.initCause(cause); throw e; } else { input = filename; } assert INPUT_TYPES.contains(input.getClass()) : input; /* * Now creates the definitive tile. The tiles in the last * row or last column may be smaller than other tiles. */ final Rectangle bounds = new Rectangle( mosaic.x + (x *= dx), mosaic.y + (y *= dy), Math.min(dx, mosaic.width - x), Math.min(dy, mosaic.height - y)); return new Tile(tile, input, bounds); } /** * 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.occurence(p) : nx*ny - count; addTo.add(tile, n); } } } /** * Adds to the given list every tiles that intersect the given region. This is * caller responsability 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 occured 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 xmin = atr.x; final int ymin = atr.y; final int xmax = atr.width + xmin; final int ymax = atr.height + ymin; /* * 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 size = addTo.size(); if (size == 0) { final int n = (xmax - xmin) * (ymax - ymin); addTo.ensureCapacity(n); } /* * Creates the destination array with a capacity equals to the maximal number of tiles * expected at this level. The array may not be filled completly if the iteration gets * some null tiles. The array way also expand belong the expected "maximal" size if we * put tiles from finer levels into the mix (as the loop below may do). */ long totalCost = 0; for (int y=ymin; y<ymax; y++) { nextTile: for (int x=xmin; x<xmax; x++) { final Tile tile = getTile(x, y); 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 = atr.width * x; atr.y = atr.height * y; assert atr.equals(tile.getAbsoluteRegion()) || !tile.getClass().equals(Tile.class) : atr; 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 occured 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 final 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(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)}. */ 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 equals 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 && Utilities.equals(this.mosaic, that.mosaic) && Utilities.equals(this.tiles, that.tiles) && Arrays .equals(this.patterns, that.patterns) && Utilities.equals(this.patternUsed, that.patternUsed) && Utilities.equals(this.finer, that.finer); } return false; } /** * Returns a hash code value for this overview level. */ @Override public int hashCode() { int code = ordinal + 37 * (xSubsampling + 37 * (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]"; } }