package org.geowebcache.arcgis.layer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.GeoWebCacheException;
import org.geowebcache.arcgis.compact.ArcGISCompactCache;
import org.geowebcache.arcgis.compact.ArcGISCompactCacheV1;
import org.geowebcache.arcgis.compact.ArcGISCompactCacheV2;
import org.geowebcache.arcgis.config.*;
import org.geowebcache.conveyor.Conveyor.CacheResult;
import org.geowebcache.conveyor.ConveyorTile;
import org.geowebcache.grid.*;
import org.geowebcache.io.FileResource;
import org.geowebcache.io.Resource;
import org.geowebcache.layer.AbstractTileLayer;
import org.geowebcache.layer.ExpirationRule;
import org.geowebcache.mime.MimeException;
import org.geowebcache.mime.MimeType;
import org.geowebcache.util.GWCVars;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
/**
* @author Gabriel Roldan
*/
public class ArcGISCacheLayer extends AbstractTileLayer {
private static final Log log = LogFactory.getLog(ArcGISCacheLayer.class);
/*
* configuration properties
*/
private Boolean enabled;
/**
* The location of the conf.xml tiling scheme configuration file
*/
private File tilingScheme;
/**
* Optional, location of the actual tiles folder. If not provided defaults to the
* {@code _alllayers} directory at the same location than the {@link #getTilingScheme()
* conf.xml} tiling scheme.
*/
private File tileCachePath;
/**
* Optional, configure whether or not the z-values should be hex-encoded or not.
* If not provided defaults to false
*/
private Boolean hexZoom;
private transient CacheInfo cacheInfo;
private transient BoundingBox layerBounds;
private String storageFormat;
private ArcGISCompactCache compactCache;
/**
* @return {@code null}, this kind of layer handles its own storage.
*/
@Override public String getBlobStoreId() {
return null;
}
@Override public boolean isEnabled() {
return enabled;
}
@Override public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public File getTilingScheme() {
return tilingScheme;
}
public void setTilingScheme(final File tilingScheme) {
this.tilingScheme = tilingScheme;
}
/**
* Returns the location of the actual tiles folder, or {@code null} if not provided, in which
* case defaults internally to the {@code _alllayers} directory at the same location than the
* {@link #getTilingScheme() conf.xml} tiling scheme.
*/
public File getTileCachePath() {
return tileCachePath;
}
/**
* Options, location of the actual tiles folder. If not provided defaults to the
* {@code _alllayers} directory at the same location than the {@link #getTilingScheme()
* conf.xml} tiling scheme.
*/
public void setTileCachePath(File tileCachePath) {
this.tileCachePath = tileCachePath;
}
public boolean isHexZoom() {
return hexZoom;
}
public void setHexZoom(boolean hexZoom) {
this.hexZoom = hexZoom;
}
/**
* @return {@code true} if success. Note this method's return type should be void. It's not
* checked anywhere
* @see org.geowebcache.layer.TileLayer#initialize(org.geowebcache.grid.GridSetBroker)
*/
@Override protected boolean initializeInternal(GridSetBroker gridSetBroker) {
if (this.enabled == null) {
this.enabled = true;
}
if (this.tilingScheme == null) {
throw new IllegalStateException(
"tilingScheme has not been set. It should point to the ArcGIS "
+ "cache tiling scheme file for this layer (conf.xml)");
}
if (tileCachePath != null) {
if (!tileCachePath.exists() || !tileCachePath.isDirectory() || !tileCachePath
.canRead()) {
throw new IllegalStateException(
"tileCachePath property for layer '" + getName() + "' is set to '"
+ tileCachePath
+ "' but the directory either does not exist or is not readable");
}
}
if (this.hexZoom == null) {
this.hexZoom = false;
}
try {
CacheInfoPersister tilingSchemeLoader = new CacheInfoPersister();
cacheInfo = tilingSchemeLoader.load(new FileReader(tilingScheme));
File layerBoundsFile = new File(tilingScheme.getParentFile(), "conf.cdi");
if (!layerBoundsFile.exists()) {
throw new RuntimeException(
"Layer bounds file not found: " + layerBoundsFile.getAbsolutePath());
}
log.info("Parsing layer bounds for " + getName());
this.layerBounds = tilingSchemeLoader.parseLayerBounds(new FileReader(layerBoundsFile));
log.info("Parsed layer bounds for " + getName() + ": " + layerBounds);
storageFormat = cacheInfo.getCacheStorageInfo().getStorageFormat();
if (storageFormat.equals(CacheStorageInfo.COMPACT_FORMAT_CODE) || storageFormat
.equals(CacheStorageInfo.COMPACT_FORMAT_CODE_V2)) {
String pathToCacheRoot = tilingScheme.getParent() + "/_alllayers";
if (tileCachePath != null)
pathToCacheRoot = tileCachePath.getAbsolutePath();
if (storageFormat.equals(CacheStorageInfo.COMPACT_FORMAT_CODE)) {
log.info(getName() + " uses compact format (ArcGIS 10.0 - 10.2)");
compactCache = new ArcGISCompactCacheV1(pathToCacheRoot);
} else if (storageFormat.equals(CacheStorageInfo.COMPACT_FORMAT_CODE_V2)) {
log.info(getName() + " uses compact format (ArcGIS 10.3)");
compactCache = new ArcGISCompactCacheV2(pathToCacheRoot);
}
}
} catch (FileNotFoundException e) {
throw new IllegalStateException(
"Tiling scheme file not found: " + tilingScheme.getAbsolutePath());
}
log.info(
"Configuring layer " + getName() + " out of the ArcGIS tiling scheme " + tilingScheme
.getAbsolutePath());
super.subSets = createGridSubsets(gridSetBroker);
super.formats = loadMimeTypes();
return true;
}
private List<MimeType> loadMimeTypes() {
String cacheTileFormat = this.cacheInfo.getTileImageInfo().getCacheTileFormat();
if ("mixed".equalsIgnoreCase(cacheTileFormat) || "jpg".equalsIgnoreCase(cacheTileFormat)) {
cacheTileFormat = "JPEG";
} else if (cacheTileFormat.toLowerCase().startsWith("png")) {
cacheTileFormat = "png";
}
cacheTileFormat = "image/" + cacheTileFormat.toLowerCase();
MimeType format;
try {
format = MimeType.createFromFormat(cacheTileFormat);
} catch (MimeException e) {
throw new RuntimeException(e);
}
return Collections.singletonList(format);
}
private Hashtable<String, GridSubset> createGridSubsets(final GridSetBroker gridSetBroker) {
final CacheInfo info = this.cacheInfo;
final TileCacheInfo tileCacheInfo = info.getTileCacheInfo();
final String layerName = getName();
final GridSetBuilder gsBuilder = new GridSetBuilder();
GridSet gridSet = gsBuilder.buildGridset(layerName, info, layerBounds);
gridSetBroker.put(gridSet);
final List<LODInfo> lodInfos = tileCacheInfo.getLodInfos();
Integer zoomStart = lodInfos.get(0).getLevelID();
Integer zoomStop = lodInfos.get(lodInfos.size() - 1).getLevelID();
GridSubset subSet = GridSubsetFactory
.createGridSubSet(gridSet, this.layerBounds, zoomStart, zoomStop);
Hashtable<String, GridSubset> subsets = new Hashtable<String, GridSubset>();
subsets.put(gridSet.getName(), subSet);
return subsets;
}
/**
* @see org.geowebcache.layer.TileLayer#getTile(org.geowebcache.conveyor.ConveyorTile)
*/
@Override public ConveyorTile getTile(final ConveyorTile tile)
throws GeoWebCacheException, IOException, OutsideCoverageException {
Resource tileContent = null;
if (storageFormat.equals(CacheStorageInfo.COMPACT_FORMAT_CODE) || storageFormat
.equals(CacheStorageInfo.COMPACT_FORMAT_CODE_V2)) {
final long[] tileIndex = tile.getTileIndex();
final String gridSetId = tile.getGridSetId();
final GridSubset gridSubset = this.getGridSubset(gridSetId);
GridSet gridSet = gridSubset.getGridSet();
final int zoom = (int) tileIndex[2];
Grid grid = gridSet.getGridLevels()[zoom];
long coverageMaxY = grid.getNumTilesHigh() - 1;
final int col = (int) tileIndex[0];
final int row = (int) (coverageMaxY - tileIndex[1]);
tileContent = compactCache.getBundleFileResource(zoom, row, col);
} else if (storageFormat.equals(CacheStorageInfo.EXPLODED_FORMAT_CODE)) {
String path = getTilePath(tile);
File tileFile = new File(path);
if (tileFile.exists()) {
tileContent = readFile(tileFile);
}
}
if (tileContent != null) {
tile.setCacheResult(CacheResult.HIT);
tile.setBlob(tileContent);
} else {
tile.setCacheResult(CacheResult.MISS);
if (!setLayerBlankTile(tile)) {
throw new OutsideCoverageException(tile.getTileIndex(), 0, 0);
}
}
// TODO Add here
saveExpirationInformation((int) (tile.getExpiresHeader() / 1000));
return tile;
}
protected void saveExpirationInformation(int backendExpire) {
this.saveExpirationHeaders = false;
try {
if (getExpireCache(0) == GWCVars.CACHE_USE_WMS_BACKEND_VALUE) {
if (backendExpire == -1) {
this.expireCacheList.set(0, new ExpirationRule(0, 7200));
log.error("Layer profile wants MaxAge from backend,"
+ " but backend does not provide this. Setting to 7200 seconds.");
} else {
this.expireCacheList.set(backendExpire, new ExpirationRule(0, 7200));
}
log.trace("Setting expireCache to: " + expireCache);
}
if (getExpireCache(0) == GWCVars.CACHE_USE_WMS_BACKEND_VALUE) {
if (backendExpire == -1) {
this.expireClientsList.set(0, new ExpirationRule(0, 7200));
log.error("Layer profile wants MaxAge from backend,"
+ " but backend does not provide this. Setting to 7200 seconds.");
} else {
this.expireClientsList.set(0, new ExpirationRule(0, backendExpire));
log.trace("Setting expireClients to: " + expireClients);
}
}
} catch (Exception e) {
// Sometimes this doesn't work (network conditions?),
// and it's really not worth getting caught up on it.
e.printStackTrace();
}
}
private boolean setLayerBlankTile(ConveyorTile tile) {
// TODO cache result
String layerPath = getLayerPath().append(File.separatorChar).toString();
File png = new File(layerPath + "blank.png");
Resource blank = null;
try {
if (png.exists()) {
blank = readFile(png);
tile.setBlob(blank);
tile.setMimeType(MimeType.createFromFormat("image/png"));
} else {
File jpeg = new File(layerPath + "missing.jpg");
if (jpeg.exists()) {
blank = readFile(jpeg);
tile.setBlob(blank);
tile.setMimeType(MimeType.createFromFormat("image/jpeg"));
}
}
} catch (Exception e) {
return false;
}
return blank != null;
}
private String getTilePath(final ConveyorTile tile) {
final MimeType mimeType = tile.getMimeType();
final long[] tileIndex = tile.getTileIndex();
final String gridSetId = tile.getGridSetId();
final GridSubset gridSubset = this.getGridSubset(gridSetId);
GridSet gridSet = gridSubset.getGridSet();
final int z = (int) tileIndex[2];
Grid grid = gridSet.getGridLevels()[z];
// long[] coverage = gridSubset.getCoverage(z);
// long coverageMinY = coverage[1];
long coverageMaxY = grid.getNumTilesHigh() - 1;
final long x = tileIndex[0];
// invert the order of the requested Y ordinate, since ArcGIS caches are top-left to
// bottom-right, and GWC computes tiles in bottom-left to top-right order
final long y = (coverageMaxY - tileIndex[1]);
String level = (this.hexZoom) ? Integer.toHexString(z) : Integer.toString(z);
level = zeroPadder(level, 2);
String row = Long.toHexString(y);
row = zeroPadder(row, 8);
String col = Long.toHexString(x);
col = zeroPadder(col, 8);
StringBuilder path = getLayerPath();
path.append(File.separatorChar).append('L').append(level).append(File.separatorChar)
.append('R').append(row).append(File.separatorChar).append('C').append(col);
String fileExtension = mimeType.getFileExtension();
if ("jpeg".equalsIgnoreCase(fileExtension)) {
fileExtension = "jpg";
}
path.append('.').append(fileExtension);
return path.toString();
}
private StringBuilder getLayerPath() {
StringBuilder path;
if (tileCachePath == null) {
path = new StringBuilder(this.tilingScheme.getParent());
// note we're assuming it's a "fused" tile cache. When it comes to support multiple
// layers
// tile caches we'll need to parametrize the layer's cache directory
path.append(File.separatorChar).append("_alllayers");
} else {
path = new StringBuilder(tileCachePath.getAbsolutePath());
}
return path;
}
private String zeroPadder(String s, int order) {
if (s.length() >= order) {
return s;
}
char[] data = new char[order];
Arrays.fill(data, '0');
for (int i = s.length() - 1, j = order - 1; i >= 0; i--, j--) {
data[j] = s.charAt(i);
}
return String.valueOf(data);
}
private Resource readFile(File fh) {
if (!fh.exists()) {
return null;
}
Resource res = new FileResource(fh);
return res;
}
/**
* @see org.geowebcache.layer.TileLayer#getNoncachedTile(org.geowebcache.conveyor.ConveyorTile)
*/
@Override public ConveyorTile getNoncachedTile(ConveyorTile tile) throws GeoWebCacheException {
throw new UnsupportedOperationException();
}
/**
* @see org.geowebcache.layer.TileLayer#seedTile(org.geowebcache.conveyor.ConveyorTile, boolean)
*/
@Override public void seedTile(ConveyorTile tile, boolean tryCache)
throws GeoWebCacheException, IOException {
throw new UnsupportedOperationException();
}
/**
* @see org.geowebcache.layer.TileLayer#doNonMetatilingRequest(org.geowebcache.conveyor.ConveyorTile)
*/
@Override public ConveyorTile doNonMetatilingRequest(ConveyorTile tile)
throws GeoWebCacheException {
throw new UnsupportedOperationException();
}
/**
* @see org.geowebcache.layer.TileLayer#getStyles()
*/
@Override public String getStyles() {
return null;
}
/**
* @see org.geowebcache.layer.TileLayer#setExpirationHeader(javax.servlet.http.HttpServletResponse,
* int)
*/
@Override public void setExpirationHeader(HttpServletResponse response, int zoomLevel) {
/*
* NOTE: this method doesn't seem like belonging to TileLayer, but to GeoWebCacheDispatcher
* itself
*/
return;
}
}