/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program 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 General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package com.bc.ceres.glevel.support; import com.bc.ceres.glevel.MultiLevelRenderer; import com.bc.ceres.glevel.MultiLevelSource; import com.bc.ceres.grender.InteractiveRendering; import com.bc.ceres.grender.Rendering; import com.bc.ceres.grender.Viewport; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.TileCache; import javax.media.jai.TileComputationListener; import javax.media.jai.TileRequest; import javax.media.jai.TileScheduler; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; public class ConcurrentMultiLevelRenderer implements MultiLevelRenderer { private final static boolean DEBUG = Boolean.getBoolean("ceres.renderer.debug"); private final Map<TileIndex, TileRequest> scheduledTileRequests; private final TileImageCache localTileCache; private final DescendingLevelsComparator descendingLevelsComparator = new DescendingLevelsComparator(); public ConcurrentMultiLevelRenderer() { scheduledTileRequests = Collections.synchronizedMap(new HashMap<TileIndex, TileRequest>(37)); localTileCache = new TileImageCache(); if (DEBUG) { final TileCache tileCache = JAI.getDefaultInstance().getTileCache(); final TileScheduler tileScheduler = JAI.getDefaultInstance().getTileScheduler(); System.out.println("jai.tileScheduler.priority = " + tileScheduler.getPriority()); System.out.println("jai.tileScheduler.parallelism = " + tileScheduler.getParallelism()); System.out.println("jai.tileScheduler.prefetchPriority = " + tileScheduler.getPrefetchPriority()); System.out.println("jai.tileScheduler.prefetchParallelism = " + tileScheduler.getPrefetchParallelism()); System.out.println("jai.tileCache.memoryCapacity = " + tileCache.getMemoryCapacity()); System.out.println("jai.tileCache.memoryThreshold = " + tileCache.getMemoryThreshold()); } } @Override public synchronized void reset() { cancelTileRequests(-1); localTileCache.clear(); } @Override public void renderImage(Rendering rendering, MultiLevelSource multiLevelSource, int currentLevel) { final long t0 = System.nanoTime(); renderImpl((InteractiveRendering) rendering, multiLevelSource, currentLevel); if (DEBUG) { final long t1 = System.nanoTime(); double time = (t1 - t0) / (1000.0 * 1000.0); System.out.printf("ConcurrentMultiLevelRenderer: render: time=%f ms, clip=%s\n", time, rendering.getGraphics().getClip()); } } private void renderImpl(InteractiveRendering rendering, MultiLevelSource multiLevelSource, int currentLevel) { final PlanarImage planarImage = (PlanarImage) multiLevelSource.getImage(currentLevel); final Graphics2D graphics = rendering.getGraphics(); final Viewport viewport = rendering.getViewport(); // Check that color model is available, required for this renderer final ColorModel colorModel = planarImage.getColorModel(); if (colorModel == null) { throw new IllegalStateException("colorModel == null"); } // Current view's bounds in view (pixel) coordinates final Rectangle viewBounds = viewport.getViewBounds(); // Check clipping rectangle in view (pixel) coordinates, required for this renderer final Rectangle clipBounds = graphics.getClipBounds(); // Create set of required tile indexes final Rectangle clippedImageRegion = getImageRegion(viewport, multiLevelSource, currentLevel, clipBounds != null ? clipBounds : viewBounds); final Set<TileIndex> requiredTileIndexes = getTileIndexes(planarImage, multiLevelSource.getImageShape(currentLevel), currentLevel, clippedImageRegion); if (requiredTileIndexes.isEmpty()) { return; // nothing to render } // Create lists of available and missing tile indexes final List<TileIndex> availableTileIndexList = new ArrayList<>(requiredTileIndexes.size()); final List<TileIndex> missingTileIndexList = new ArrayList<>(requiredTileIndexes.size()); final List<TileIndex> notScheduledTileIndexList = new ArrayList<>(requiredTileIndexes.size()); for (TileIndex requiredTileIndex : requiredTileIndexes) { if (localTileCache.contains(requiredTileIndex)) { availableTileIndexList.add(requiredTileIndex); } else { missingTileIndexList.add(requiredTileIndex); if (!scheduledTileRequests.containsKey(requiredTileIndex)) { notScheduledTileIndexList.add(requiredTileIndex); } } } // Schedule missing tiles, if any if (!notScheduledTileIndexList.isEmpty()) { final TileScheduler tileScheduler = JAI.getDefaultInstance().getTileScheduler(); final TileComputationHandler tileComputationHandler = new TileComputationHandler(rendering, multiLevelSource, currentLevel); final TileRequest tileRequest = tileScheduler.scheduleTiles(planarImage, getPoints(notScheduledTileIndexList), new TileComputationListener[]{ tileComputationHandler } ); for (TileIndex tileIndex : notScheduledTileIndexList) { scheduledTileRequests.put(tileIndex, tileRequest); } } // Draw missing tiles from other levels (if any) drawTentativeTileImages(graphics, viewport, multiLevelSource, currentLevel, planarImage, missingTileIndexList); // Draw available tiles for (final TileIndex tileIndex : availableTileIndexList) { final TileImage tileImage = localTileCache.get(tileIndex); drawTileImage(graphics, viewport, tileImage); } if (DEBUG) { // Draw tile frames final AffineTransform i2m = multiLevelSource.getModel().getImageToModelTransform(currentLevel); drawTileImageFrames(graphics, viewport, availableTileIndexList, i2m, Color.YELLOW); drawTileFrames(graphics, viewport, planarImage, missingTileIndexList, i2m, Color.RED); drawTileFrames(graphics, viewport, planarImage, availableTileIndexList, i2m, Color.BLUE); } // Cancel any pending tile requests that are not in the visible region final Rectangle visibleImageRegion = getImageRegion(viewport, multiLevelSource, currentLevel, viewBounds); final Set<TileIndex> visibleTileIndexSet = getTileIndexes(planarImage, multiLevelSource.getImageShape(currentLevel), currentLevel, visibleImageRegion); if (!visibleTileIndexSet.isEmpty()) { cancelTileRequests(visibleTileIndexSet); } localTileCache.adjustTrimSize(planarImage, visibleTileIndexSet.size()); // Remove any tile images that are older than the retention period. localTileCache.trim(currentLevel, visibleTileIndexSet); } private void drawTentativeTileImages(Graphics2D g, Viewport vp, MultiLevelSource multiLevelSource, int level, PlanarImage planarImage, List<TileIndex> missingTileIndexList) { final AffineTransform i2m = multiLevelSource.getModel().getImageToModelTransform(level); for (final TileIndex tileIndex : missingTileIndexList) { final Rectangle tileRect = planarImage.getTileRect(tileIndex.tileX, tileIndex.tileY); final Rectangle2D bounds = i2m.createTransformedShape(tileRect).getBounds2D(); final TreeSet<TileImage> tentativeTileImageSet = new TreeSet<>(descendingLevelsComparator); final Collection<TileImage> tileImages = localTileCache.getAll(); // Search for a tile image at the nearest higher resolution which is contained by bounds TileImage containedTileImage = null; int containedLevel = Integer.MAX_VALUE; for (TileImage tileImage : tileImages) { final int someLevel = tileImage.tileIndex.level; if (someLevel > level && someLevel < containedLevel && tileImage.bounds.contains(bounds)) { containedTileImage = tileImage; containedLevel = someLevel; } } if (containedTileImage != null) { tentativeTileImageSet.add(containedTileImage); // Search for intersecting tile images at a higher resolution for (TileImage tileImage : tileImages) { if (tileImage.tileIndex.level < level && tileImage.bounds.intersects(bounds)) { tentativeTileImageSet.add(tileImage); } } } else { // Search for intersecting tile images at any resolution for (TileImage tileImage : tileImages) { if (tileImage.tileIndex.level != level && tileImage.bounds.intersects(bounds)) { tentativeTileImageSet.add(tileImage); } } } final Shape oldClip = g.getClip(); Rectangle newClip = vp.getModelToViewTransform().createTransformedShape(bounds).getBounds(); newClip = newClip.intersection(vp.getViewBounds()); g.setClip(newClip); for (TileImage tileImage : tentativeTileImageSet) { drawTileImage(g, vp, tileImage); } g.setClip(oldClip); } } private static Point[] getPoints(List<TileIndex> tileIndexList) { final Point[] points = new Point[tileIndexList.size()]; for (int i = 0; i < tileIndexList.size(); i++) { TileIndex tileIndex = tileIndexList.get(i); points[i] = new Point(tileIndex.tileX, tileIndex.tileY); } return points; } private static Set<TileIndex> getTileIndexes(PlanarImage planarImage, Shape imageShape, int level, Rectangle clippedImageRegion) { final Point[] indices = planarImage.getTileIndices(clippedImageRegion); if (indices == null || indices.length == 0) { return Collections.emptySet(); } final Set<TileIndex> indexes = new HashSet<>((3 * indices.length) / 2); for (Point point : indices) { Rectangle tileRect = planarImage.getTileRect(point.x, point.y); if (imageShape == null || imageShape.intersects(tileRect)) { indexes.add(new TileIndex(point.x, point.y, level)); } } return indexes; } private static void drawTileImage(Graphics2D g, Viewport vp, TileImage ti) { final AffineTransform t = AffineTransform.getTranslateInstance(ti.x, ti.y); t.preConcatenate(ti.i2m); t.preConcatenate(vp.getModelToViewTransform()); g.drawRenderedImage(ti.image, t); ti.lastAccessTime = System.currentTimeMillis(); } private void drawTileImageFrames(Graphics2D g, Viewport vp, List<TileIndex> tileIndices, AffineTransform i2m, Color frameColor) { final AffineTransform m2v = vp.getModelToViewTransform(); final AffineTransform oldTransform = g.getTransform(); final Color oldColor = g.getColor(); final Stroke oldStroke = g.getStroke(); final AffineTransform t = new AffineTransform(); t.preConcatenate(i2m); t.preConcatenate(m2v); g.setTransform(t); g.setColor(new Color(frameColor.getRed(), frameColor.getGreen(), frameColor.getBlue(), 127)); g.setStroke(new BasicStroke(5.0f)); for (final TileIndex tileIndex : tileIndices) { final TileImage tileImage = localTileCache.get(tileIndex); final Rectangle tileRect = new Rectangle(tileImage.x, tileImage.y, tileImage.image.getWidth(), tileImage.image.getHeight()); g.draw(tileRect); System.out.println("Tile image bounds: " + tileRect); } g.setStroke(oldStroke); g.setColor(oldColor); g.setTransform(oldTransform); } private static void drawTileFrames(Graphics2D g, Viewport vp, PlanarImage planarImage, List<TileIndex> tileIndices, AffineTransform i2m, Color frameColor) { final AffineTransform m2v = vp.getModelToViewTransform(); final AffineTransform oldTransform = g.getTransform(); final Color oldColor = g.getColor(); final Stroke oldStroke = g.getStroke(); final AffineTransform t = new AffineTransform(); t.preConcatenate(i2m); t.preConcatenate(m2v); g.setTransform(t); g.setColor(frameColor); g.setStroke(new BasicStroke(1.0f)); for (TileIndex tileIndex : tileIndices) { g.draw(planarImage.getTileRect(tileIndex.tileX, tileIndex.tileY)); } g.setStroke(oldStroke); g.setColor(oldColor); g.setTransform(oldTransform); } // Called from EDT. // Cancels any tiles that are in the scheduled list and not in the visibleTileIndexSet list. private void cancelTileRequests(Set<TileIndex> visibleTileIndexSet) { final Map<TileIndex, TileRequest> scheduledTileRequestsCopy; synchronized (scheduledTileRequests) { scheduledTileRequestsCopy = new HashMap<>(scheduledTileRequests); } // scan through the scheduled tiles list cancelling any that are no longer in view for (Map.Entry<TileIndex, TileRequest> scheduledTileEntry : scheduledTileRequestsCopy.entrySet()) { TileIndex scheduledTileIndex = scheduledTileEntry.getKey(); if (!visibleTileIndexSet.contains(scheduledTileIndex)) { TileRequest request = scheduledTileEntry.getValue(); // if tile not already removed (concurrently) if (request != null) { scheduledTileRequests.remove(scheduledTileIndex); request.cancelTiles(new Point[]{new Point(scheduledTileIndex.tileX, scheduledTileIndex.tileY)}); } } } } private void cancelTileRequests(int currentLevel) { final Map<TileIndex, TileRequest> scheduledTileRequestsCopy; synchronized (scheduledTileRequests) { scheduledTileRequestsCopy = new HashMap<>(scheduledTileRequests); } for (Map.Entry<TileIndex, TileRequest> entry : scheduledTileRequestsCopy.entrySet()) { TileIndex tileIndex = entry.getKey(); if (tileIndex.level != currentLevel) { scheduledTileRequests.remove(tileIndex); entry.getValue().cancelTiles(null); } } } private static TileImage createTileImage(GraphicsConfiguration deviceConfiguration, PlanarImage planarImage, TileIndex tileIndex, Raster tile, AffineTransform i2m) { final RenderedImage image = createDeviceCompatibleImageForTile(deviceConfiguration, planarImage, tileIndex, tile); return new TileImage(image, tileIndex, planarImage.tileXToX(tileIndex.tileX), planarImage.tileYToY(tileIndex.tileY), i2m); } private static RenderedImage createDeviceCompatibleImageForTile(GraphicsConfiguration deviceConfiguration, PlanarImage planarImage, TileIndex tileIndex, Raster tile) { final SampleModel sm = planarImage.getSampleModel(); final ColorModel cm = planarImage.getColorModel(); final Rectangle r = planarImage.getTileRect(tileIndex.tileX, tileIndex.tileY); final DataBuffer db = tile.getDataBuffer(); final WritableRaster wr = Raster.createWritableRaster(sm, db, null); final BufferedImage bi = new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null); //System.out.println("bi = " + bi); if (r.width == tile.getWidth() && r.height == tile.getHeight() && deviceConfiguration.getColorModel().isCompatibleRaster(wr)) { return bi; } // todo: Optimize me! // The following code might still be too slow. Try to use use JAI "format" and "crop" operations instead of // matching color model and tile bounds via a BufferedImage. We don't need to create a BufferedImage // then, because the resulting RenderedOp can be drawn directly using g.drawRenderedImage() final BufferedImage bi2 = deviceConfiguration.createCompatibleImage(r.width, r.height, bi.getTransparency()); final Graphics2D g = bi2.createGraphics(); g.drawRenderedImage(bi, null); g.dispose(); return bi2; } private static Rectangle getImageRegion(Viewport vp, MultiLevelSource multiLevelSource, int level, Rectangle2D viewRegion) { return getViewToImageTransform(vp, multiLevelSource, level).createTransformedShape(viewRegion).getBounds(); } private static Rectangle getViewRegion(Viewport vp, MultiLevelSource multiLevelSource, int level, Rectangle2D imageRegion) { return getImageToViewTransform(vp, multiLevelSource, level).createTransformedShape(imageRegion).getBounds(); } private static AffineTransform getViewToImageTransform(Viewport vp, MultiLevelSource multiLevelSource, int level) { final AffineTransform t = vp.getViewToModelTransform(); t.preConcatenate(multiLevelSource.getModel().getModelToImageTransform(level)); return t; } private static AffineTransform getImageToViewTransform(Viewport vp, MultiLevelSource multiLevelSource, int level) { final AffineTransform t = new AffineTransform(multiLevelSource.getModel().getImageToModelTransform(level)); t.preConcatenate(vp.getModelToViewTransform()); return t; } private static int compareAscending(TileImage ti1, TileImage ti2) { int d = ti1.tileIndex.level - ti2.tileIndex.level; if (d != 0) { return d; } d = ti1.tileIndex.tileY - ti2.tileIndex.tileY; if (d != 0) { return d; } d = ti1.tileIndex.tileX - ti2.tileIndex.tileX; if (d != 0) { return d; } return 0; } private class TileComputationHandler implements TileComputationListener { private final InteractiveRendering rendering; private final GraphicsConfiguration deviceConfiguration; private final MultiLevelSource multiLevelSource; private final int level; private TileComputationHandler(InteractiveRendering rendering, MultiLevelSource multiLevelSource, int level) { this.rendering = rendering; this.deviceConfiguration = rendering.getGraphics().getDeviceConfiguration(); this.multiLevelSource = multiLevelSource; this.level = level; } // Called from worker threads of the tile scheduler. @Override public void tileComputed(Object object, TileRequest[] tileRequests, PlanarImage planarImage, int tileX, int tileY, Raster tile) { if (tile == null) { if (DEBUG) { System.out.println("WARNING: tileComputed: tile == null!"); } return; } TileIndex tileIndex = new TileIndex(tileX, tileY, level); // Check whether tile is still required or has been canceled already if (!scheduledTileRequests.containsKey(tileIndex)) { // todo - problem here if this renderer is shared by multiple views (nf, 20081216) //return; } final TileImage tileImage = createTileImage(deviceConfiguration, planarImage, tileIndex, tile, multiLevelSource.getModel().getImageToModelTransform(level)); synchronized (ConcurrentMultiLevelRenderer.this) { scheduledTileRequests.remove(tileIndex); localTileCache.add(tileImage); } // Uncomment for debugging // if (DEBUG) { // try { // Thread.sleep(100); // } catch (InterruptedException e) { // // don't care // } // } final Rectangle tileBounds = tile.getBounds(); // Invoke in the EDT in order to obtain the // viewRegion for the currently valid viewport settings since the model // may have changed the viewport between the time the tile request was // created and the tile was computed, which is now. The EDT is the only safe // place to access the viewport. rendering.invokeLater(new Runnable() { // Called from EDT. @Override public void run() { final Rectangle viewRegion = getViewRegion(rendering.getViewport(), multiLevelSource, level, tileBounds); rendering.invalidateRegion(viewRegion); } }); } // Called from worker threads of the tile scheduler. @Override public void tileCancelled(Object object, TileRequest[] tileRequests, PlanarImage planarImage, int tileX, int tileY) { TileIndex tileIndex = new TileIndex(tileX, tileY, level); dropTile(tileIndex); if (DEBUG) { System.out.printf("ConcurrentMultiLevelRenderer: tileCancelled: %s\n", tileIndex); } } // Called from worker threads of the tile scheduler. @Override public void tileComputationFailure(Object object, TileRequest[] tileRequests, PlanarImage planarImage, int tileX, int tileY, Throwable error) { TileIndex tileIndex = new TileIndex(tileX, tileY, level); dropTile(tileIndex); if (DEBUG) { System.out.printf("ConcurrentMultiLevelRenderer: tileComputationFailure: %s\n", tileIndex); error.printStackTrace(); } } private void dropTile(TileIndex tileIndex) { synchronized (ConcurrentMultiLevelRenderer.this) { scheduledTileRequests.remove(tileIndex); localTileCache.remove(tileIndex); } } } private final class TileImageCache { private final Map<TileIndex, TileImage> cache; private long currentSize; private long trimSize; private final boolean adaptive; private final double tileFactor; private final long minSize; private final long maxSize; private final long retentionPeriod; public TileImageCache() { cache = new HashMap<>(37); retentionPeriod = Long.parseLong(System.getProperty("ceres.renderer.cache.retentionPeriod", "10000")); adaptive = Boolean.parseBoolean(System.getProperty("ceres.renderer.cache.adaptive", "true")); tileFactor = Double.parseDouble(System.getProperty("ceres.renderer.cache.tileFactor", "2.5")); minSize = Long.parseLong(System.getProperty("ceres.renderer.cache.minSize", "0")) * (1024 * 1024); maxSize = Long.parseLong(System.getProperty("ceres.renderer.cache.maxSize", System.getProperty("ceres.renderer.cache.capacity", "64"))) * (1024 * 1024); currentSize = 0; trimSize = maxSize; } public synchronized boolean contains(TileIndex tileIndex) { return cache.containsKey(tileIndex); } public synchronized TileImage get(TileIndex tileIndex) { return cache.get(tileIndex); } public synchronized Collection<TileImage> getAll() { return new ArrayList<>(cache.values()); } public synchronized void add(TileImage tileImage) { final TileImage oldTileImage = cache.put(tileImage.tileIndex, tileImage); if (oldTileImage != null) { currentSize -= oldTileImage.size; } currentSize += tileImage.size; if (DEBUG) { System.out.printf("ConcurrentMultiLevelRenderer$TileImageCache: add: tileIndex=%s, size=%d\n", tileImage.tileIndex, currentSize); } } public synchronized void remove(TileIndex tileIndex) { final TileImage oldTileImage = cache.remove(tileIndex); if (oldTileImage != null) { currentSize -= oldTileImage.size; if (DEBUG) { System.out.printf("ConcurrentMultiLevelRenderer$TileImageCache: remove: tileIndex=%s, size=%d\n", tileIndex, currentSize); } } } public synchronized void clear() { cache.clear(); currentSize = 0L; } public void adjustTrimSize(PlanarImage image, int numRequiredTiles) { if (adaptive) { SampleModel sm = image.getSampleModel(); long pixelSize = (long) (sm.getNumBands() * sm.getSampleSize(0)) / 8; long tileSize = (long) sm.getWidth() * (long) sm.getHeight() * pixelSize; long trimSize = Math.round(tileFactor * numRequiredTiles) * tileSize; if (minSize >= 0 && trimSize < minSize) { trimSize = minSize; } if (maxSize >= 0 && trimSize > maxSize) { trimSize = maxSize; } this.trimSize = trimSize; } else { this.trimSize = maxSize; } } public synchronized void trim(int currentLevel, Set<TileIndex> visibleTileIndexes) { if (DEBUG) { long oneMiB = 1024L * 1024L; System.out.println("ConcurrentMultiLevelRenderer.TileImageCache:"); System.out.printf(" currentSize = %10d%n", localTileCache.currentSize / oneMiB); System.out.printf(" trimSize = %10d%n", localTileCache.trimSize / oneMiB); System.out.printf(" minSize = %10d%n", localTileCache.minSize / oneMiB); System.out.printf(" maxSize = %10d%n", localTileCache.maxSize / oneMiB); System.out.printf(" tileFactor = %f%n", localTileCache.tileFactor); System.out.printf(" retentionPeriod = %d%n", localTileCache.retentionPeriod); } if (currentSize > trimSize) { long now = System.currentTimeMillis(); Collection<TileImage> tileImages = new ArrayList<>(cache.values()); for (TileImage tileImage : tileImages) { if (!visibleTileIndexes.contains(tileImage.tileIndex) && tileImage.tileIndex.level != currentLevel) { maybeRemove(tileImage, now); } } for (TileImage tileImage : tileImages) { if (!visibleTileIndexes.contains(tileImage.tileIndex) && tileImage.tileIndex.level == currentLevel) { maybeRemove(tileImage, now); } } } } private void maybeRemove(TileImage image, long now) { final long age = now - image.lastAccessTime; if (age > retentionPeriod) { remove(image.tileIndex); } } } private final static class TileImage { private final RenderedImage image; private final TileIndex tileIndex; /** * x offset in image CS */ private final int x; /** * y offset in image CS */ private final int y; /** * image-to-model transformation */ private final AffineTransform i2m; /** * tile bounds in model CS */ private final Rectangle2D bounds; /** * tile size in bytes */ private final long size; /** * last access time stamp */ private long lastAccessTime; private TileImage(RenderedImage image, TileIndex tileIndex, int x, int y, AffineTransform i2m) { this.image = image; this.tileIndex = tileIndex; this.x = x; this.y = y; this.i2m = new AffineTransform(i2m); this.bounds = i2m.createTransformedShape(new Rectangle(x, y, image.getWidth(), image.getHeight())).getBounds2D(); this.size = image.getWidth() * image.getHeight() * (image.getSampleModel().getNumBands() * image.getSampleModel().getSampleSize(0)) / 8; this.lastAccessTime = System.currentTimeMillis(); } @Override public String toString() { return String.format("TileImage[tileIndex=%s,size=%d,bounds=%s]", String.valueOf(tileIndex), size, String.valueOf(bounds)); } @Override public boolean equals(Object object) { if (this == object) { return true; } if (object == null || getClass() != object.getClass()) { return false; } final TileImage tileImage = (TileImage) object; return tileIndex.equals(tileImage.tileIndex); } @Override public int hashCode() { return tileIndex.hashCode(); } } private final static class TileIndex { private final int tileX; private final int tileY; private final int level; private TileIndex(int tileX, int tileY, int level) { this.tileX = tileX; this.tileY = tileY; this.level = level; } Point getPoint() { return new Point(tileX, tileY); } @Override public boolean equals(Object object) { if (this == object) { return true; } if (object == null || getClass() != object.getClass()) { return false; } TileIndex tileIndex = (TileIndex) object; return level == tileIndex.level && tileY == tileIndex.tileY && tileX == tileIndex.tileX; } @Override public int hashCode() { return 31 * (31 * level + tileY) + tileX; } @Override public String toString() { return String.format("TileIndex[tileX=%d,tileY=%d,level=%d]", tileX, tileY, level); } } private static class DescendingLevelsComparator implements Comparator<TileImage> { @Override public int compare(TileImage ti1, TileImage ti2) { return compareAscending(ti2, ti1); } } }