/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.eas.client.controls.geopane.cache.webtiles;
import com.eas.client.controls.geopane.GeoPaneUtils;
import com.eas.client.controls.geopane.TileUtils;
import com.eas.client.controls.geopane.TilesBoundaries;
import com.eas.client.controls.geopane.cache.AsyncMapTilesCache;
import com.eas.util.BinaryUtils;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import org.geotools.map.MapContent;
/**
*
* @author mg
*/
public abstract class AsyncWebMapTilesCache extends AsyncMapTilesCache {
public static final String DEFAULT_VECTOR_LAYER_NAME = "vec";
public static final String DEFAULT_SAT_LAYER_NAME = "sat";
public static final String DEFAULT_HYBRID_LAYER_NAME = "hybrid";
public static final String TEMP_DIR_PROP_NAME = "java.io.tmpdir";
public static final String WEB_TILE_FILE_NAME_TEMPLATE = "x%d_y%d.png";
protected static final double MERCATOR_WORLD_LENGTH = 4.007501668557849e+7;
protected static final int WEB_TILES_CACHE_MAX_SIZE = 128;
protected String tilesServerUrl;
// level of the tiles in multi layer tiles storage of yandex or google or other maps service.
protected int tilesLevel = 7;
protected int tileInnerPixelSize = 256;
protected static final Map<WebTileKey, Image> webTilesCache = new ConcurrentHashMap<>();
protected Image urledPlaceholderImage;
protected String backingLayerName = DEFAULT_VECTOR_LAYER_NAME;
public String getBackingUrl() {
return tilesServerUrl;
}
protected abstract String formatTileUrl(WebTileKey aTileKey);
public abstract void configureVectorDomains(int aMin, int aMax);
public abstract void configureSatteliteDomains(int aMin, int aMax);
protected class AsyncThirdPartyRenderingTask extends AsyncRenderingTask {
public AsyncThirdPartyRenderingTask(Point aTilePoint) throws NoninvertibleTransformException {
super(aTilePoint);
}
@Override
protected void prepareImageTile(BufferedImage image) {
if (tilesServerUrl != null) {
Graphics2D g = image.createGraphics();
try {
try2RenderTileBack(g, tilesLevel);
// the following algorithm consumes too much resources
/*
int dLevel = 0;
while (tilesLevel >= dLevel && try2RenderTileBack(g, tilesLevel - dLevel) > 0) {
dLevel++;
}
*/
} finally {
g.dispose();
}
} else {
super.prepareImageTile(image);
}
}
protected int try2RenderTileBack(Graphics2D g, int aTilesLevel) {
int badTiles = 0;
Point2D.Double ptTopLeft = new Point2D.Double(tileAoi.getMinX(), tileAoi.getMinY());
Point2D.Double ptBottomRight = new Point2D.Double(tileAoi.getMaxX(), tileAoi.getMaxY());
Rectangle2D.Double cartesianTileAoi = new Rectangle2D.Double(ptTopLeft.x, ptTopLeft.y, ptBottomRight.x - ptTopLeft.x, ptBottomRight.y - ptTopLeft.y);
double cart2ScreenCoef = (double) tileSize / cartesianTileAoi.width;
cartesianTileAoi.x += MERCATOR_WORLD_LENGTH / 2;
cartesianTileAoi.y += MERCATOR_WORLD_LENGTH / 2;
double cartesianTileSize = MERCATOR_WORLD_LENGTH / Math.pow(2, aTilesLevel);
TilesBoundaries tiles = calcTilesBoundaries(cartesianTileAoi, cartesianTileSize);
Rectangle tilesTileAoi = convertCartesianTileAoi2TilesAoi(cartesianTileAoi, aTilesLevel);
for (int tileX = tiles.minX; tileX <= tiles.maxX; tileX++) {
for (int tileY = tiles.minY; tileY <= tiles.maxY; tileY++) {
Rectangle imageRect = TileUtils.calcImageRect(tileX, tileY, tilesTileAoi, tileInnerPixelSize);
Rectangle2D.Double controlRect2D = TileUtils.calcControlRect(tileX, tileY, cartesianTileAoi, cartesianTileSize);
controlRect2D.x -= cartesianTileAoi.x;
controlRect2D.y -= cartesianTileAoi.y;
Rectangle controlRect = new Rectangle(
Double.valueOf(Math.floor(controlRect2D.x * cart2ScreenCoef)).intValue(),
Double.valueOf(Math.floor(controlRect2D.y * cart2ScreenCoef)).intValue(),
Double.valueOf(Math.ceil(controlRect2D.width * cart2ScreenCoef)).intValue(),
Double.valueOf(Math.ceil(controlRect2D.height * cart2ScreenCoef)).intValue());
Image urledImage = achieveUrledTileImage(tileX, tileY, aTilesLevel);
if (urledImage != null) {
if (urledImage == urledPlaceholderImage) {
badTiles++;
}
int lx = 0, rx = 0, ty = 0, by = 0;
if (tileX == tiles.minX && controlRect.x > 0) {
lx = controlRect.x;
}
if (tileX == tiles.maxX && controlRect.x + controlRect.width < getTileSize() - 1) {
rx = getTileSize() - 1 - controlRect.x + controlRect.width;
}
if (tileY == tiles.minY && controlRect.y > 0) {
ty = controlRect.y;
}
if (tileY == tiles.maxY && controlRect.y + controlRect.height < getTileSize() - 1) {
by = getTileSize() - 1 - controlRect.y + controlRect.height;
}
g.drawImage(urledImage, controlRect.x - lx, controlRect.y - ty, controlRect.x + controlRect.width + lx + rx, controlRect.y + controlRect.height + ty + by,
imageRect.x - lx, imageRect.y - ty, imageRect.x + imageRect.width + lx + rx, imageRect.y + imageRect.height + ty + by, null);
}
}
}
return badTiles;
}
protected Rectangle convertCartesianTileAoi2TilesAoi(Rectangle2D.Double aCartesianAoi, int aTilesLevel) {
double pixelsByWorld = Math.pow(2, aTilesLevel) * (double) tileInnerPixelSize;
double pixelByMeter = pixelsByWorld / MERCATOR_WORLD_LENGTH;
return new Rectangle(
Double.valueOf(aCartesianAoi.x * pixelByMeter).intValue(),
Double.valueOf(aCartesianAoi.y * pixelByMeter).intValue(),
Double.valueOf(Math.ceil(aCartesianAoi.width * pixelByMeter)).intValue(),
Double.valueOf(Math.ceil(aCartesianAoi.height * pixelByMeter)).intValue());
}
protected TilesBoundaries calcTilesBoundaries(Rectangle2D.Double areaOfInterest, double aTileSize) {
TilesBoundaries bounds = new TilesBoundaries();
bounds.minX = Double.valueOf(Math.floor(areaOfInterest.x / aTileSize)).intValue();
bounds.maxX = Double.valueOf(Math.floor((areaOfInterest.x + areaOfInterest.width) / aTileSize)).intValue();
bounds.minY = Double.valueOf(Math.floor(areaOfInterest.y / aTileSize)).intValue();
bounds.maxY = Double.valueOf(Math.floor((areaOfInterest.y + areaOfInterest.height) / aTileSize)).intValue();
return bounds;
}
}
protected static class WebTileKey extends Object {
public int x;
public int y;
public int z;
public WebTileKey(int aX, int aY, int aZ) {
super();
x = aX;
y = aY;
z = aZ;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final WebTileKey other = (WebTileKey) obj;
if (this.x != other.x) {
return false;
}
if (this.y != other.y) {
return false;
}
return this.z == other.z;
}
@Override
public int hashCode() {
int hash = 7;
hash = 37 * hash + this.x;
hash = 37 * hash + this.y;
hash = 37 * hash + this.z;
return hash;
}
@Override
public String toString() {
return "Web tile x:" + String.valueOf(x) + "; y:" + String.valueOf(y) + "; z:" + String.valueOf(z);
}
}
public AsyncWebMapTilesCache(int aCacheSize, MapContent aDisplayContext, ReadWriteLock aMapContextLock, AffineTransform aTransform) {
super(aCacheSize, aDisplayContext, aMapContextLock, aTransform);
}
public AsyncWebMapTilesCache(String aBaseUrl, MapContent aDisplayContext, ReadWriteLock aMapContextLock, AffineTransform aTransform) {
super(aDisplayContext, aMapContextLock, aTransform);
tilesServerUrl = aBaseUrl;
}
public String getBackingLayerName() {
return backingLayerName;
}
public void setBackingLayerName(String aBackingLayerName) {
backingLayerName = aBackingLayerName;
}
@Override
public void scaleChanged() {
super.scaleChanged();
double scale = cartesian2ScreenTransform.getScaleX();
double scaledWorldLength = scale * MERCATOR_WORLD_LENGTH;
double tilesCount = scaledWorldLength / (double) tileInnerPixelSize;
double tilesLevelOfInterest = Math.log(tilesCount) / Math.log(2);
tilesLevel = Long.valueOf(Math.round(tilesLevelOfInterest)).intValue();
constraintTilesLevel();
}
@Override
protected Image renderTile(Point ptKey) {
try {
AsyncRenderingTask task = new AsyncThirdPartyRenderingTask(ptKey);
offeredTasks.put(ptKey, task);
executor.execute(task);
return null;
} catch (NoninvertibleTransformException ex) {
Logger.getLogger(AsyncMapTilesCache.class.getName()).log(Level.SEVERE, null, ex);
return null;
}
}
protected Image achieveUrledTileImage(int x, int y, int z) {
WebTileKey tKey = new WebTileKey(x, y, z);
synchronized (webTilesCache) {
Image cached = webTilesCache.get(tKey);
if (cached == null) {
cached = loadUrledTileImage(tKey);
if (webTilesCache.size() > WEB_TILES_CACHE_MAX_SIZE) {
webTilesCache.clear();
}
webTilesCache.put(tKey, cached);
}
return cached;
}
}
/**
* Loads image from Web tiles index (like yandex, google, yahoo) or
* from disk file cache.
* @param aTileKey
* @return Image achieved.
* WARNING! Detalization level is NOT zoom of any kind.
* Although Web documentation names z as zoom level, it's not true!
* It's only detailization level and it affects only on world size in pixels of target data.
* Furthermore, in such indexes image tile is not rendering helper or cache it is data.
*/
protected Image loadUrledTileImage(WebTileKey aTileKey) {
Image urledImage = null;
try {
String filePath = constructCachePath() + File.separator + calcTileFileName(aTileKey);
forceCreatePath(filePath.substring(0, filePath.lastIndexOf(File.separator)));
File f = new File(filePath);
if (f.exists()) {
try (FileInputStream fi = new FileInputStream(f)) {
byte[] imageData = BinaryUtils.readStream(fi, -1);
ImageIcon icon = new ImageIcon(imageData);
urledImage = icon.getImage();
}
} else {
String urlWithParams = formatTileUrl(aTileKey);
Logger.getLogger(AsyncWebMapTilesCache.class.getName()).log(Level.FINE, "Formatted web tile url is: {0}", new Object[]{urlWithParams});
URL url = new URL(urlWithParams);
URLConnection connection = url.openConnection();
try {
try (InputStream wi = connection.getInputStream()) {
byte[] imageData = BinaryUtils.readStream(wi, -1);
ImageIcon icon = new ImageIcon(imageData);
urledImage = icon.getImage();
if (!f.exists()) {
try (FileOutputStream fo = new FileOutputStream(f)) {
fo.write(imageData);
}
}
}
} finally {
}
}
} catch (IOException ex) {
Logger.getLogger(AsyncWebMapTilesCache.class.getName()).log(Level.SEVERE, "{0} is unavailable. The cause is: {1}", new Object[]{aTileKey.toString(), ex.toString()});
ceckPlaceHolderImage();
urledImage = urledPlaceholderImage;
}
return urledImage;
}
protected String calcTileFileName(WebTileKey aTileKey) {
String plainFileName = String.format(WEB_TILE_FILE_NAME_TEMPLATE, aTileKey.x, aTileKey.y);
String dirPrefix = "z" + String.valueOf(aTileKey.z) + File.separator + backingLayerName;
int mod = Double.valueOf(Math.pow(10, aTileKey.z - 5)).intValue();
if (aTileKey.z > 5 && aTileKey.x * aTileKey.y > mod) {
String modDirPrefix = String.valueOf(aTileKey.x * aTileKey.y % mod);
return dirPrefix + File.separator + modDirPrefix + File.separator + plainFileName;
} else {
return dirPrefix + File.separator + plainFileName;
}
}
protected void forceCreateDirectory(String aPath) {
File file = new File(aPath);
if (!file.exists()) {
file.mkdir();
}
}
protected String constructCachePath() {
String cachedTilesPath = System.getProperty(TEMP_DIR_PROP_NAME);
if (!cachedTilesPath.endsWith(File.separator)) {
cachedTilesPath += File.separator;
}
cachedTilesPath += ".eas";
cachedTilesPath += File.separator + "mapsTilesCache";
cachedTilesPath += File.separator + getWebIndexName();
return cachedTilesPath;
}
protected void forceCreatePath(String aPath) {
String sep = File.separator;
if (sep.equals("\\")) {
sep = "\\" + sep;
}
String[] pathElements = aPath.split(sep);
String forcedPath = null;
for (String pathElement : pathElements) {
if (forcedPath != null) {
forcedPath += File.separator;
}
if (forcedPath == null) {
forcedPath = "";
}
forcedPath += pathElement;
forceCreateDirectory(forcedPath);
}
}
protected void ceckPlaceHolderImage() {
if (urledPlaceholderImage == null) {
urledPlaceholderImage = new BufferedImage(tileInnerPixelSize, tileInnerPixelSize, BufferedImage.TYPE_INT_RGB);
Graphics g = urledPlaceholderImage.getGraphics();
g.setColor(background);
g.fillRect(0, 0, tileInnerPixelSize, tileInnerPixelSize);
g.setColor(Color.gray);
g.drawString(GeoPaneUtils.getString("backingUnavailable"), 2, tileInnerPixelSize / 2);
}
}
protected abstract String getWebIndexName();
protected abstract void constraintTilesLevel();
}