/**
* This program 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, 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 Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Arne Kepp, OpenGeo, Copyright 2009
*/
package org.geowebcache.filter.request;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Hashtable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.GeoWebCacheException;
import org.geowebcache.conveyor.ConveyorTile;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.grid.OutsideCoverageException;
import org.geowebcache.layer.TileLayer;
/**
* A raster filter allows to optimize data loading by avoiding the generation of requests and the
* caching of empty tiles for tiles that are inside the definition area of the layer but that are
* known (via external information) to contain no data
*
* To conserve memory, the layer bounds are used.
*
* The raster must match the dimensions of the zoomlevel and use 0x000000 for tiles that are valid.
*/
public abstract class RasterFilter extends RequestFilter {
private static final long serialVersionUID = -5695649347572928323L;
private static Log log = LogFactory.getLog(RasterFilter.class);
private Integer zoomStart;
private Integer zoomStop;
private Boolean resample;
private Boolean preload;
private Boolean debug;
public transient Hashtable<String, BufferedImage[]> matrices;
public RasterFilter() {
}
/**
* @return the zoomStart
*/
public Integer getZoomStart() {
return zoomStart;
}
/**
* @param zoomStart
* the zoomStart to set
*/
public void setZoomStart(Integer zoomStart) {
this.zoomStart = zoomStart;
}
/**
* @return the zoomStop
*/
public Integer getZoomStop() {
return zoomStop;
}
/**
* @param zoomStop
* the zoomStop to set
*/
public void setZoomStop(Integer zoomStop) {
this.zoomStop = zoomStop;
}
/**
* @return the resample
*/
public Boolean getResample() {
return resample;
}
/**
* @param resample
* the resample to set
*/
public void setResample(Boolean resample) {
this.resample = resample;
}
/**
* @return the preload
*/
public Boolean getPreload() {
return preload;
}
/**
* @param preload
* the preload to set
*/
public void setPreload(Boolean preload) {
this.preload = preload;
}
/**
* @return the debug
*/
public Boolean getDebug() {
return debug;
}
/**
* @param debug
* the debug to set
*/
public void setDebug(Boolean debug) {
this.debug = debug;
}
public void apply(ConveyorTile convTile) throws RequestFilterException {
long[] idx = convTile.getTileIndex().clone();
String gridSetId = convTile.getGridSetId();
// Basic bounds test first
try {
convTile.getGridSubset().checkCoverage(idx);
} catch (OutsideCoverageException oce) {
throw new BlankTileException(this);
}
int zoomDiff = 0;
// Three scenarios below:
// 1. z is too low , upsample if resampling is enabled
// 2. z is within range, downsample one level and apply
// 3. z is too large , downsample
if (zoomStart != null && idx[2] < zoomStart) {
if (resample == null || !resample) {
// Filter does not apply, zoomlevel is too low
return;
} else {
// Upsample
zoomDiff = (int) (idx[2] - zoomStart);
idx[0] = idx[0] << (-1 * zoomDiff);
idx[1] = idx[1] << (-1 * zoomDiff);
idx[2] = zoomStart;
}
} else if (idx[2] < zoomStop) {
// Sample one level higher
idx[0] = idx[0] * 2;
idx[1] = idx[1] * 2;
idx[2] = idx[2] + 1;
} else {
// Reduce to highest supported resolution
zoomDiff = (int) (idx[2] - zoomStop);
idx[0] = idx[0] >> zoomDiff;
idx[1] = idx[1] >> zoomDiff;
idx[2] = zoomStop;
}
if (matrices == null || matrices.get(gridSetId) == null
|| matrices.get(gridSetId)[(int) idx[2]] == null) {
try {
setMatrix(convTile.getLayer(), gridSetId, (int) idx[2], false);
} catch (Exception e) {
log.error("Failed to load matrix for " + this.getName() + ", " + gridSetId + ", "
+ idx[2] + " : " + e.getMessage());
throw new RequestFilterException(this, 500,
"Failed while trying to load filter for " + idx[2]
+ ", please check the logs");
}
}
if (zoomDiff == 0) {
if (!lookup(convTile.getGridSubset(), idx)) {
if (debug != null && debug) {
throw new GreenTileException(this);
} else {
throw new BlankTileException(this);
}
}
} else if (zoomDiff > 0) {
if (!lookupQuad(convTile.getGridSubset(), idx)) {
if (debug != null && debug) {
throw new GreenTileException(this);
} else {
throw new BlankTileException(this);
}
}
} else if (zoomDiff < 0) {
if (!lookupSubsample(convTile.getGridSubset(), idx, zoomDiff)) {
if (debug != null && debug) {
throw new GreenTileException(this);
} else {
throw new BlankTileException(this);
}
}
}
}
/**
* Loops over all the zoom levels and initializes the lookup images.
*/
public void initialize(TileLayer layer) throws GeoWebCacheException {
if (preload != null && preload) {
for (String gridSetId : layer.getGridSubsets()) {
GridSubset grid = layer.getGridSubset(gridSetId);
for (int i = 0; i <= zoomStop; i++) {
try {
setMatrix(layer, grid.getName(), i, false);
} catch (Exception e) {
log.error("Failed to load matrix for " + this.getName() + ", "
+ grid.getName() + ", " + i + " : " + e.getMessage());
}
}
}
}
}
/**
* Performs a lookup against an internal raster.
*
* @param grid
* @param idx
* @return
*/
private boolean lookup(GridSubset grid, long[] idx) {
BufferedImage mat = matrices.get(grid.getName())[(int) idx[2]];
long[] gridCoverage = grid.getCoverage((int) idx[2]);
// Changing index to top left hand origin
long x = idx[0] - gridCoverage[0];
long y = gridCoverage[3] - idx[1];
return (mat.getRaster().getSample((int) x, (int) y, 0) == 0);
}
/**
* Performs a lookup against an internal raster. The sampling is actually done against 4 pixels,
* idx should already have been modified to use one level higher than strictly necessary.
*
* @param grid
* @param idx
* @return
*/
private boolean lookupQuad(GridSubset grid, long[] idx) {
BufferedImage mat = matrices.get(grid.getName())[(int) idx[2]];
long[] gridCoverage = grid.getCoverage((int) idx[2]);
// Changing index to top left hand origin
int baseX = (int) (idx[0] - gridCoverage[0]);
int baseY = (int) (gridCoverage[3] - idx[1]);
int width = mat.getWidth();
int height = mat.getHeight();
int x = baseX;
int y = baseY;
// We're checking 4 samples. The base is bottom left hand corner
boolean hasData = false;
// BL, BR, TL, TR
int[] xOffsets = { 0, 1, 0, 1 };
int[] yOffsets = { 0, 0, 1, 1 };
// Lock, in case someone wants to replace the matrix
synchronized (mat) {
try {
for (int i = 0; i < 4 && !hasData; i++) {
x = baseX + xOffsets[i];
y = baseY - yOffsets[i];
if (x > -1 && x < width && y > -1 && y < height) {
if (mat.getRaster().getSample(x, y, 0) == 0) {
hasData = true;
}
}
}
} catch (ArrayIndexOutOfBoundsException aioob) {
log.error("x:" + x + " y:" + y + " (" + mat.getWidth() + " " + mat.getHeight()
+ ")");
}
}
return hasData;
}
private boolean lookupSubsample(GridSubset grid, long[] idx, int zoomDiff) {
BufferedImage mat = matrices.get(grid.getName())[(int) idx[2]];
int sampleChange = 1 << (-1 * zoomDiff);
long[] gridCoverage = grid.getCoverage((int) idx[2]);
// Changing index to top left hand origin
int baseX = (int) (idx[0] - gridCoverage[0]);
int baseY = (int) (gridCoverage[3] - idx[1]);
int width = mat.getWidth();
int height = mat.getHeight();
int startX = Math.max(0, baseX);
int stopX = Math.min(width, baseX + sampleChange);
int startY = Math.min(baseY, height - 1);
int stopY = Math.max(0, baseY - sampleChange);
int x = -1;
int y = -1;
// Lock, in case someone wants to replace the matrix
synchronized (mat) {
try {
// Try center and edges first
x = (stopX + startX) / 2;
y = (startY + stopY) / 2;
if (mat.getRaster().getSample(x, y, 0) == 0
|| mat.getRaster().getSample(stopX - 1, stopY + 1, 0) == 0
|| mat.getRaster().getSample(stopX - 1, startY, 0) == 0
|| mat.getRaster().getSample(startX, stopY + 1, 0) == 0) {
return true;
}
// Do the hard work, loop over all pixels
x = startX;
y = startY;
// Left to right
while (x < stopX) {
// Bottom to top
while (y > stopY) {
if (mat.getRaster().getSample(x, y, 0) == 0) {
return true;
}
y--;
}
x++;
y = startY;
}
} catch (ArrayIndexOutOfBoundsException aioob) {
log.error("x:" + x + " y:" + y + " (" + mat.getWidth() + " " + mat.getHeight()
+ ")");
}
}
return false;
}
/**
* This function will load the matrix from the appropriate source.
*
* @param layer
* Access to the layer, to make the object simpler
* @param srs
* @param z
* (zoom level)
* @param replace
* Whether to update if a matrix exists
*/
public synchronized void setMatrix(TileLayer layer, String gridSetId, int z, boolean replace)
throws IOException, GeoWebCacheException {
if (matrices == null) {
matrices = new Hashtable<String, BufferedImage[]>();
}
if (matrices.get(gridSetId) == null) {
matrices.put(gridSetId, new BufferedImage[zoomStop + 1]);
}
if (matrices.get(gridSetId)[z] == null) {
matrices.get(gridSetId)[z] = loadMatrix(layer, gridSetId, z);
} else if (replace) {
BufferedImage oldImg = matrices.get(gridSetId)[z];
BufferedImage[] matArray = matrices.get(gridSetId);
// Get the replacement
BufferedImage newImg = loadMatrix(layer, gridSetId, z);
// We need to lock it
synchronized (oldImg) {
matArray[z] = newImg;
}
}
}
/**
* Helper function for calculating width and height
*
* @param grid
* @param z
* @return
* @throws GeoWebCacheException
*/
protected int[] calculateWidthHeight(GridSubset grid, int z) throws GeoWebCacheException {
long[] bounds = grid.getCoverage(z);
int[] widthHeight = new int[2];
widthHeight[0] = (int) (bounds[2] - bounds[0] + 1);
widthHeight[1] = (int) (bounds[3] - bounds[1] + 1);
return widthHeight;
}
protected abstract BufferedImage loadMatrix(TileLayer layer, String gridSetId, int zoomLevel)
throws IOException, GeoWebCacheException;
}