/******************************************************************************* * Copyright (c) MOBAC developers * * 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 2 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 mobac.program.atlascreators.tileprovider; import java.awt.image.BufferedImage; import java.io.IOException; import java.lang.ref.SoftReference; import java.util.Hashtable; import java.util.concurrent.LinkedBlockingQueue; import mobac.program.interfaces.MapSource; import org.apache.log4j.Logger; /** * A tile cache with speculative loading on a separate thread. Usually this decreases map generation time on multi-core * systems. */ public class CacheTileProvider implements TileProvider { private Logger log = Logger.getLogger(CacheTileProvider.class); /** * Counter for identifying the different threads */ private static int PRELOADER_THREAD_NUM = 1; private Hashtable<CacheKey, SRCachedTile> cache; private PreLoadThread preLoader = new PreLoadThread(); protected final TileProvider tileProvider; public CacheTileProvider(TileProvider tileProvider) { this.tileProvider = tileProvider; cache = new Hashtable<CacheKey, SRCachedTile>(500); preLoader.start(); } public boolean preferTileImageUsage() { return true; } public BufferedImage getTileImage(int x, int y) throws IOException { SRCachedTile cachedTile = cache.get(new CacheKey(x, y)); BufferedImage image = null; if (cachedTile != null) { CachedTile tile = cachedTile.get(); if (tile != null) { if (tile.loaded) log.trace(String.format("Cache hit: x=%d y=%d", x, y)); image = tile.getImage(); if (!tile.nextLoadJobCreated) { // log.debug(String.format("Preload job added : x=%d y=%d l=%d", // x + 1, y, layer)); preloadTile(new CachedTile(new CacheKey(x + 1, y))); tile.nextLoadJobCreated = true; } } } if (image == null) { log.trace(String.format("Cache miss: x=%d y=%d", x, y)); // log.debug(String.format("Preload job added : x=%d y=%d l=%d", x + // 1, y, layer)); preloadTile(new CachedTile(new CacheKey(x + 1, y))); image = internalGetTileImage(x, y); } return image; } protected BufferedImage internalGetTileImage(int x, int y) throws IOException { synchronized (tileProvider) { return tileProvider.getTileImage(x, y); } } public byte[] getTileData(int layer, int x, int y) throws IOException { throw new RuntimeException("Not implemented"); } public byte[] getTileData(int x, int y) throws IOException { throw new RuntimeException("Not implemented"); } public MapSource getMapSource() { return tileProvider.getMapSource(); } private void preloadTile(CachedTile tile) { if (preLoader.queue.remainingCapacity() < 1) { // Preloader thread is too slow log.trace("Preloading rejected: " + tile.key); return; } if (cache.get(tile.key) != null) return; try { preLoader.queue.add(tile); cache.put(tile.key, new SRCachedTile(tile)); } catch (IllegalStateException e) { // Queue is "full" log.trace("Preloading rejected: " + tile.key); } } public void cleanup() { try { cache.clear(); if (preLoader != null) { preLoader.interrupt(); preLoader = null; } } catch (Throwable t) { log.error("", t); } } @Override protected void finalize() throws Throwable { cleanup(); super.finalize(); } private static class SRCachedTile extends SoftReference<CachedTile> { public SRCachedTile(CachedTile referent) { super(referent); } } private class PreLoadThread extends Thread { private LinkedBlockingQueue<CachedTile> queue = null; public PreLoadThread() { super("ImagePreLoadThread" + (PRELOADER_THREAD_NUM++)); log.debug("Image pre-loader thread started"); // pre-loading more than 20 tiles doesn't make much sense queue = new LinkedBlockingQueue<CachedTile>(20); } @Override public void run() { CachedTile tile; try { while (true) { tile = queue.take(); if (tile != null && !tile.loaded) { // log.trace("Loading image async: " + tile); tile.loadImage(); } } } catch (InterruptedException e) { log.debug("Image pre-loader thread terminated"); } } } private static class CacheKey { int x; int y; public CacheKey(int x, int y) { super(); this.x = x; this.y = y; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; result = prime * result + y; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; CacheKey other = (CacheKey) obj; if (x != other.x) return false; if (y != other.y) return false; return true; } @Override public String toString() { return "CacheKey [x=" + x + ", y=" + y + "]"; } } private class CachedTile { CacheKey key; private BufferedImage image; private IOException loadException = null; boolean loaded = false; boolean nextLoadJobCreated = false; public CachedTile(CacheKey key) { super(); this.key = key; image = null; } public synchronized void loadImage() { try { image = internalGetTileImage(key.x, key.y); } catch (IOException e) { loadException = e; } catch (Exception e) { loadException = new IOException(e); } loaded = true; } public synchronized BufferedImage getImage() throws IOException { if (!loaded) loadImage(); if (loadException != null) throw loadException; return image; } @Override public String toString() { return "CachedTile [key=" + key + ", loaded=" + loaded + ", nextLoadJobCreated=" + nextLoadJobCreated + "]"; } } }