package edu.kit.pse.ws2013.routekit.mapdisplay; import java.awt.Color; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.lang.ref.SoftReference; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; /** * Manages the calculation of map tiles and caches them. Tiles can be requested, * and after the (asynchronous) calculation is finished, registered * {@link TileFinishedListener TileFinishedListeners} are notified. * <p> * Internally, cached tiles are held in a way that permits the Garbage Collector * to discard them in the case of memory shortage (via {@link SoftReference * SoftReferences}). */ public class TileCache implements TileSource { private class TileJob implements Runnable { private int x; private int y; private int zoom; public TileJob(int x, int y, int zoom) { this.x = x; this.y = y; this.zoom = zoom; } @Override public void run() { if (zoom < 0 || zoom > 19) { return; } String key = key(x, y, zoom); if (cache.containsKey(key) && cache.get(key).get() != null) { return; } BufferedImage result = target.renderTile(x, y, zoom); cache.put(key, new SoftReference<BufferedImage>(result)); fireListeners(x, y, zoom, result); } } private class Worker extends Thread { public Worker(String name) { super(name); setDaemon(true); } @Override public void run() { while (running) { while (waiting.size() > 100) {// too much work waiting.removeLast(); } while (prefetchWaiting.size() > 100) {// too much work prefetchWaiting.removeLast(); } try { // returns null if empty TileJob job = waiting.pollFirst(); if (job == null) { // immediately returns null if empty job = prefetchWaiting.pollFirst(200, TimeUnit.MILLISECONDS); } if (job != null) { job.run(); } } catch (InterruptedException e) { // its ok... } } } } private boolean running = true; private LinkedBlockingDeque<TileJob> waiting = new LinkedBlockingDeque<>(); private LinkedBlockingDeque<TileJob> prefetchWaiting = new LinkedBlockingDeque<>(); private LinkedList<TileFinishedListener> listeners = new LinkedList<>(); private TileSource target; private Map<String, SoftReference<BufferedImage>> cache = Collections .synchronizedMap(new HashMap<String, SoftReference<BufferedImage>>()); private Worker[] workers; BufferedImage tile; /** * Creates a new {@link TileCache} which uses the given {@link TileSource} * for calculating tiles. * * @param target * The {@link TileSource} that actually renders the tiles, and * whose results are cached. */ public TileCache(TileSource target) { this.target = target; int workerCount = Runtime.getRuntime().availableProcessors(); workers = new Worker[workerCount]; for (int i = 0; i < workerCount; i++) { workers[i] = new Worker("TileCache Worker " + i); workers[i].start(); } tile = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB); Graphics g = tile.createGraphics(); g.setColor(Color.gray); g.fillRect(1, 1, 254, 254); } /** * If the requested tile is cached and available, it is returned directly; * otherwise, a “dummy” tile is returned and the correct one is in turn * requested (asynchronously) from the internal {@link TileSource}. When the * calculation is finished, the resulting tile is cached and registered * {@link TileFinishedListener TileFinishedListeners} are notified. In any * case, this method returns immediately. * <p> * Additionally, tiles surrounding the requested tile (in all 6 directions) * are also requested and cached (with lower priority), since it is likely * that they will be requested soon as well (locality of reference). * * @param x * The SMT X component. * @param y * The SMT Y component. * @param zoom * The zoom level. * @return Either a dummy tile or a cached tile. */ @Override public BufferedImage renderTile(int x, int y, int zoom) { final int bitmask = (1 << zoom) - 1; x &= bitmask; y &= bitmask; String key = key(x, y, zoom); SoftReference<BufferedImage> cacheVal = cache.get(key); BufferedImage tile; if (cacheVal != null && (tile = cacheVal.get()) != null) { prefetchEnv(x, y, zoom); return tile; } waiting.addFirst(new TileJob(x, y, zoom)); prefetchEnv(x, y, zoom); return this.tile; } private void prefetchEnv(int x, int y, int zoom) { prefetch(x + 1, y, zoom); prefetch(x - 1, y, zoom); prefetch(x, y + 1, zoom); prefetch(x, y - 1, zoom); if (zoom > 0) { prefetch(x / 2, y / 2, zoom - 1); } if (zoom < 19) { prefetch(x * 2, y * 2, zoom + 1); prefetch(x * 2 + 1, y * 2, zoom + 1); prefetch(x * 2 + 1, y * 2 + 1, zoom + 1); prefetch(x * 2, y * 2 + 1, zoom + 1); } } private void prefetch(int x, int y, int zoom) { final int bitmask = (1 << zoom) - 1; x &= bitmask; y &= bitmask; SoftReference<BufferedImage> ref = cache.get(key(x, y, zoom)); if (ref == null || ref.get() == null) { prefetchWaiting.addFirst(new TileJob(x, y, zoom)); } } /** * Registers a {@link TileFinishedListener} that is notified when * calculation of a tile has finished. The tile itself is part of the * notification. * * @param listener * The listener that shall be added. */ public void addTileFinishedListener(TileFinishedListener listener) { listeners.add(listener); } private void fireListeners(int x, int y, int zoom, BufferedImage tile) { for (TileFinishedListener list : listeners) { list.tileFinished(x, y, zoom, tile); } } public TileSource getTarget() { return target; } private static String key(int x, int y, int zoom) { return x + "/" + y + "/" + zoom; } /** * Causes this TileCache to stop fetching. */ public void stop() { running = false; for (int i = 0; i < workers.length; i++) { workers[i].interrupt(); } } /** * Causes this TileCache to stop fetching and waits for total termination. * * @throws InterruptedException * if the waiting is interrupted. if tw */ public void waitForStop() throws InterruptedException { stop(); for (int i = 0; i < workers.length; i++) { workers[i].join(); } } }