/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2012-2015, 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.large;
import java.awt.Point;
import java.awt.image.ImagingOpException;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.TileCache;
import org.apache.sis.util.logging.Logging;
/**
* Manage {@link RenderedImage} and its {@link Raster} to don't exceed JVM memory capacity.
*
* TODO : make memory be entirely managed by the cache, instead of allow a portion of
* memory to each {@link org.geotoolkit.image.io.large.ImageTilesCache}.
* The aim is to just delegate tile manipulation to them, and get the total control over memory here.
* Maybe a priority system would be useful to determine which tile to release first (based on the number
* of times a tile has been queried ?)
*
* @author Rémi Maréchal (Geomatys)
* @author Alexis Manin (Geomatys)
*/
public final class LargeCache implements TileCache {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.image.io.large");
private final ReferenceQueue<RenderedImage> phantomQueue = new ReferenceQueue<>();
private volatile long memoryCapacity;
private final boolean enableSwap;
/**
* Contains a tile manager for each cached rendered image. A tile manager job is to swap / cache image tiles as we ask it.
*
* Note : we can not use ReentrantReadWriteLock with WeakHashMap because
* get and iteration methods may modify the content of the map.
* This is because the weak references are tested when a get occurs.
*/
private final WeakHashMap<RenderedImage, ImageTilesCache> tileManagers = new WeakHashMap<>();
//We MUST keep hard references to the largemaps, otherwise the dispose wont be called
//by the reference queue. check the javadoc for more details.
private final Set<ImageTilesCache> largemaps = new HashSet<>();
private static LargeCache INSTANCE;
private LargeCache(long memoryCapacity, boolean enableSwap) {
this.memoryCapacity = memoryCapacity;
this.enableSwap = enableSwap;
final Thread phantomCleaner = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
final ImageTilesCache removed = (ImageTilesCache) phantomQueue.remove();
largemaps.remove(removed);
removed.removeTiles();
// Re-distribute freed memory amount between remaining caches.
updateLList();
} catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Reference cleaner has been interrupted ! It could cause severe memory leaks.");
return;
} catch (Throwable t) {
LOGGER.log(Level.WARNING, "An image reference cannot be released. It's likely to cause memory leaks !");
}
}
}
});
phantomCleaner.setName("LargeCache cleaner deamon");
phantomCleaner.setDaemon(true);
phantomCleaner.start();
}
boolean isEnableSwap() {
return enableSwap;
}
long getCacheSizePerImage(){
synchronized(tileManagers){
return memoryCapacity / (tileManagers.size() + 1);
}
}
/**
* <p>Construct tile cache mechanic.<br/>
* Stock Raster while memory capacity does not exceed else write in temporary file.</p>
*
* @return TileCache
*/
public static synchronized LargeCache getInstance() {
if(INSTANCE==null){
final long memoryCapacity = ImageCacheConfiguration.getCacheMemorySize();
final boolean enableSwap = ImageCacheConfiguration.isCacheSwapEnable();
INSTANCE = new LargeCache(memoryCapacity, enableSwap);
}
return INSTANCE;
}
/**
* Return the cache system associated to the given rendered image. If there's no
* such thing, it will be created / referenced then returned.
* @param source The image we want data from.
* @return The found or creeated cache system.
* @throws IOException If the image did not have any cache system, and we cannot create one.
*/
private ImageTilesCache getOrCreateLargeMap(final RenderedImage source) throws IOException {
ImageTilesCache lL;
synchronized (source) {
synchronized(tileManagers){
lL = tileManagers.get(source);
}
if (lL == null) {
try {
lL = new ImageTilesCache(source, phantomQueue, this);
} catch (IOException ex) {
throw new RuntimeException("impossible to create cache list", ex);
}
synchronized(tileManagers){
tileManagers.put(source, lL);
largemaps.add(lL);
}
updateLList();
}
}
return lL;
}
/**
* {@inheritDoc }.
*/
@Override
public void add(RenderedImage ri, int tileX, int tileY, Raster raster) {
// TODO : check existing tile, flush it before replacing it, or do nothing.
if (!(raster instanceof WritableRaster)) {
throw new IllegalArgumentException("raster must be WritableRaster instance");
}
try {
final ImageTilesCache lL = getOrCreateLargeMap(ri);
lL.add(tileX, tileY, (WritableRaster) raster);
} catch (IOException ex) {
throw new RuntimeException("impossible to add raster (write raster on disk)", ex);
}
}
/**
* {@inheritDoc }.
*/
@Override
public void remove(RenderedImage ri, int tileX, int tileY) {
final ImageTilesCache lL;
synchronized(tileManagers){
lL = tileManagers.get(ri);
}
if (lL == null){
throw new IllegalArgumentException("renderedImage don't exist in this "+LargeCache.class.getName());
}
lL.remove(tileX, tileY);
}
/**
* {@inheritDoc }.
* @throws java.lang.IllegalArgumentException if TileCache is in memoryMode only and the
* requested raster is not found on cache.
* @throws java.lang.RuntimeException if raster can't be retrieve from cache (nested IOException).
*/
@Override
public Raster getTile(RenderedImage ri, int tileX, int tileY) {
final ImageTilesCache cache;
synchronized(tileManagers){
cache = tileManagers.get(ri);
}
if (cache == null){
throw new IllegalArgumentException("renderedImage doesn't exist in this "+LargeCache.class.getName());
}
try {
return cache.getRaster(tileX, tileY);
} catch (IOException ex) {
throw (RuntimeException)(new ImagingOpException(ex.getMessage()).initCause(ex));
}
}
/**
* {@inheritDoc }.
*/
@Override
public void removeTiles(RenderedImage ri) {
final ImageTilesCache lL;
// De-reference image
synchronized(tileManagers){
lL = tileManagers.remove(ri);
}
// Clear cache.
if (lL != null) {
try {
lL.removeTiles();
} catch (IOException ex) {
throw new RuntimeException("Raster too large for remaining memory capacity", ex);
}
updateLList();
}
}
/**
* {@inheritDoc }.
*/
@Override
public void addTiles(RenderedImage ri, Point[] points, Raster[] rasters, Object o) {
if (points.length != rasters.length)
throw new IllegalArgumentException("point and raster tables must have same length.");
final ImageTilesCache lL;
try {
lL = getOrCreateLargeMap(ri);
} catch (IOException e) {
throw new RuntimeException("There is no cache system for the given image, and we cannot create any.", e);
}
for (int id = 0, l = points.length; id < l; id++) {
if (!(rasters[id] instanceof WritableRaster))
throw new IllegalArgumentException("raster must be WritableRaster instance");
try {
lL.add(points[id].x, points[id].y, (WritableRaster) rasters[id]);
} catch (IOException ex) {
throw new RuntimeException("impossible to add raster (write raster on disk)", ex);
}
}
}
/**
* {@inheritDoc }.
*/
@Override
public Raster[] getTiles(RenderedImage ri, Point[] points) {
final ImageTilesCache lL;
synchronized(tileManagers){
lL = tileManagers.get(ri);
}
if (lL == null)
throw new IllegalArgumentException("renderedImage don't exist in this "+LargeCache.class.getName());
final int l = points.length;
final Raster[] rasters = new Raster[l];
for (int id = 0; id < l; id++) {
try {
rasters[id] = lL.getRaster(points[id].x, points[id].y);
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Unreadable tile : "+points[id], ex);
}
}
return rasters;
}
/**
* {@inheritDoc }.
*/
@Override
public void setMemoryCapacity(long l) {
this.memoryCapacity = l;
updateLList();
}
/**
* Affect a new memory capacity and update {@link Raster} list from new memory capacity set.
* TODO : delete this method when memory capacity will be entirely managed by {@link org.geotoolkit.image.io.large.LargeCache}.
* @param listMemoryCapacity new memory capacity.
*/
private void updateLList() {
Object[] array;
synchronized(tileManagers){
array = tileManagers.values().toArray();
}
for (Object lL : array) {
try {
((ImageTilesCache)lL).capacityChanged();
} catch (IOException ex) {
throw new RuntimeException("Raster too large for remaining memory capacity", ex);
}
}
}
/**
* {@inheritDoc }.
*/
@Override
public long getMemoryCapacity() {
return memoryCapacity;
}
/*
* UNSUPPORTED OPERATIONS
*/
/**
* {@inheritDoc }.
*/
@Override
public Raster[] getTiles(RenderedImage ri) {
throw new UnsupportedOperationException("Not supported yet.");
// if (!tileManagers.containsKey(ri))
// throw new IllegalArgumentException("renderedImage don't exist in this "+LargeCache.class.getName());
// try {
// return tileManagers.get(ri).getTiles();
// } catch (IOException ex) {
// Logging.getLogger("org.geotoolkit.image.io.large").log(Level.SEVERE, null, ex);
// }
// return null;
}
/**
* {@inheritDoc }.
*/
@Override
public void setMemoryThreshold(float f) {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public float getMemoryThreshold() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public void setTileComparator(Comparator cmprtr) {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public Comparator getTileComparator() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public void flush() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public void memoryControl() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
@Deprecated
public void setTileCapacity(int i) {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
@Deprecated
public int getTileCapacity() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* {@inheritDoc }.
*/
@Override
public void add(RenderedImage ri, int i, int i1, Raster raster, Object o) {
throw new UnsupportedOperationException("Not supported yet.");
}
}