// License: GPL. For details, see Readme.txt file.
package org.openstreetmap.gui.jmapviewer;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.imageio.ImageIO;
import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
/**
* Holds one map tile. Additionally the code for loading the tile image and
* painting it is also included in this class.
*
* @author Jan Peter Stotz
*/
public class Tile {
/**
* Hourglass image that is displayed until a map tile has been loaded, except for overlay sources
*/
public static final BufferedImage LOADING_IMAGE = loadImage("images/hourglass.png");
/**
* Red cross image that is displayed after a loading error, except for overlay sources
*/
public static final BufferedImage ERROR_IMAGE = loadImage("images/error.png");
protected TileSource source;
protected int xtile;
protected int ytile;
protected int zoom;
protected BufferedImage image;
protected String key;
protected volatile boolean loaded; // field accessed by multiple threads without any monitors, needs to be volatile
protected volatile boolean loading;
protected volatile boolean error;
protected String error_message;
/** TileLoader-specific tile metadata */
protected Map<String, String> metadata;
/**
* Creates a tile with empty image.
*
* @param source Tile source
* @param xtile X coordinate
* @param ytile Y coordinate
* @param zoom Zoom level
*/
public Tile(TileSource source, int xtile, int ytile, int zoom) {
this(source, xtile, ytile, zoom, LOADING_IMAGE);
}
/**
* Creates a tile with specified image.
*
* @param source Tile source
* @param xtile X coordinate
* @param ytile Y coordinate
* @param zoom Zoom level
* @param image Image content
*/
public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) {
this.source = source;
this.xtile = xtile;
this.ytile = ytile;
this.zoom = zoom;
this.image = image;
this.key = getTileKey(source, xtile, ytile, zoom);
}
private static BufferedImage loadImage(String path) {
try {
return ImageIO.read(JMapViewer.class.getResourceAsStream(path));
} catch (IOException | IllegalArgumentException ex) {
ex.printStackTrace();
return null;
}
}
private static class CachedCallable<V> implements Callable<V> {
private V result;
private Callable<V> callable;
/**
* Wraps callable so it is evaluated only once
* @param callable to cache
*/
CachedCallable(Callable<V> callable) {
this.callable = callable;
}
@Override
public synchronized V call() {
try {
if (result == null) {
result = callable.call();
}
return result;
} catch (Exception e) {
// this should not happen here
throw new RuntimeException(e);
}
}
}
/**
* Tries to get tiles of a lower or higher zoom level (one or two level
* difference) from cache and use it as a placeholder until the tile has been loaded.
* @param cache Tile cache
*/
public void loadPlaceholderFromCache(TileCache cache) {
/*
* use LazyTask as creation of BufferedImage is very expensive
* this way we can avoid object creation until we're sure it's needed
*/
final CachedCallable<BufferedImage> tmpImage = new CachedCallable<>(new Callable<BufferedImage>() {
@Override
public BufferedImage call() throws Exception {
return new BufferedImage(source.getTileSize(), source.getTileSize(), BufferedImage.TYPE_INT_ARGB);
}
});
for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) {
// first we check if there are already the 2^x tiles
// of a higher detail level
int zoomHigh = zoom + zoomDiff;
if (zoomDiff < 3 && zoomHigh <= JMapViewer.MAX_ZOOM) {
int factor = 1 << zoomDiff;
int xtileHigh = xtile << zoomDiff;
int ytileHigh = ytile << zoomDiff;
final double scale = 1.0 / factor;
/*
* use LazyTask for graphics to avoid evaluation of tmpImage, until we have
* something to draw
*/
CachedCallable<Graphics2D> graphics = new CachedCallable<>(new Callable<Graphics2D>() {
@Override
public Graphics2D call() throws Exception {
Graphics2D g = (Graphics2D) tmpImage.call().getGraphics();
g.setTransform(AffineTransform.getScaleInstance(scale, scale));
return g;
}
});
int paintedTileCount = 0;
for (int x = 0; x < factor; x++) {
for (int y = 0; y < factor; y++) {
Tile tile = cache.getTile(source, xtileHigh + x, ytileHigh + y, zoomHigh);
if (tile != null && tile.isLoaded()) {
paintedTileCount++;
tile.paint(graphics.call(), x * source.getTileSize(), y * source.getTileSize());
}
}
}
if (paintedTileCount == factor * factor) {
image = tmpImage.call();
return;
}
}
int zoomLow = zoom - zoomDiff;
if (zoomLow >= JMapViewer.MIN_ZOOM) {
int xtileLow = xtile >> zoomDiff;
int ytileLow = ytile >> zoomDiff;
final int factor = 1 << zoomDiff;
final double scale = factor;
CachedCallable<Graphics2D> graphics = new CachedCallable<>(new Callable<Graphics2D>() {
@Override
public Graphics2D call() throws Exception {
Graphics2D g = (Graphics2D) tmpImage.call().getGraphics();
AffineTransform at = new AffineTransform();
int translateX = (xtile % factor) * source.getTileSize();
int translateY = (ytile % factor) * source.getTileSize();
at.setTransform(scale, 0, 0, scale, -translateX, -translateY);
g.setTransform(at);
return g;
}
});
Tile tile = cache.getTile(source, xtileLow, ytileLow, zoomLow);
if (tile != null && tile.isLoaded()) {
tile.paint(graphics.call(), 0, 0);
image = tmpImage.call();
return;
}
}
}
}
public TileSource getSource() {
return source;
}
/**
* Returns the X coordinate.
* @return tile number on the x axis of this tile
*/
public int getXtile() {
return xtile;
}
/**
* Returns the Y coordinate.
* @return tile number on the y axis of this tile
*/
public int getYtile() {
return ytile;
}
/**
* Returns the zoom level.
* @return zoom level of this tile
*/
public int getZoom() {
return zoom;
}
/**
* @return tile indexes of the top left corner as TileXY object
*/
public TileXY getTileXY() {
return new TileXY(xtile, ytile);
}
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public void loadImage(InputStream input) throws IOException {
setImage(ImageIO.read(input));
}
/**
* @return key that identifies a tile
*/
public String getKey() {
return key;
}
public boolean isLoaded() {
return loaded;
}
public boolean isLoading() {
return loading;
}
public void setLoaded(boolean loaded) {
this.loaded = loaded;
}
public String getUrl() throws IOException {
return source.getTileUrl(zoom, xtile, ytile);
}
/**
* Paints the tile-image on the {@link Graphics} <code>g</code> at the
* position <code>x</code>/<code>y</code>.
*
* @param g the Graphics object
* @param x x-coordinate in <code>g</code>
* @param y y-coordinate in <code>g</code>
*/
public void paint(Graphics g, int x, int y) {
if (image == null)
return;
g.drawImage(image, x, y, null);
}
/**
* Paints the tile-image on the {@link Graphics} <code>g</code> at the
* position <code>x</code>/<code>y</code>.
*
* @param g the Graphics object
* @param x x-coordinate in <code>g</code>
* @param y y-coordinate in <code>g</code>
* @param width width that tile should have
* @param height height that tile should have
*/
public void paint(Graphics g, int x, int y, int width, int height) {
if (image == null)
return;
g.drawImage(image, x, y, width, height, null);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Tile ").append(key);
if (loading) {
sb.append(" [LOADING...]");
}
if (loaded) {
sb.append(" [loaded]");
}
if (error) {
sb.append(" [ERROR]");
}
return sb.toString();
}
/**
* Note that the hash code does not include the {@link #source}.
* Therefore a hash based collection can only contain tiles
* of one {@link #source}.
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + xtile;
result = prime * result + ytile;
result = prime * result + zoom;
return result;
}
/**
* Compares this object with <code>obj</code> based on
* the fields {@link #xtile}, {@link #ytile} and
* {@link #zoom}.
* The {@link #source} field is ignored.
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Tile other = (Tile) obj;
if (xtile != other.xtile)
return false;
if (ytile != other.ytile)
return false;
if (zoom != other.zoom)
return false;
if (!getTileSource().equals(other.getTileSource())) {
return false;
}
return true;
}
public static String getTileKey(TileSource source, int xtile, int ytile, int zoom) {
return zoom + "/" + xtile + "/" + ytile + "@" + source.getName();
}
public String getStatus() {
if (this.error)
return "error";
if (this.loaded)
return "loaded";
if (this.loading)
return "loading";
return "new";
}
public boolean hasError() {
return error;
}
public String getErrorMessage() {
return error_message;
}
public void setError(Exception e) {
setError(e.toString());
}
public void setError(String message) {
error = true;
setImage(ERROR_IMAGE);
error_message = message;
}
/**
* Puts the given key/value pair to the metadata of the tile.
* If value is null, the (possibly existing) key/value pair is removed from
* the meta data.
*
* @param key Key
* @param value Value
*/
public void putValue(String key, String value) {
if (value == null || value.isEmpty()) {
if (metadata != null) {
metadata.remove(key);
}
return;
}
if (metadata == null) {
metadata = new HashMap<>();
}
metadata.put(key, value);
}
/**
* returns the metadata of the Tile
*
* @param key metadata key that should be returned
* @return null if no such metadata exists, or the value of the metadata
*/
public String getValue(String key) {
if (metadata == null) return null;
return metadata.get(key);
}
/**
*
* @return metadata of the tile
*/
public Map<String, String> getMetadata() {
if (metadata == null) {
metadata = new HashMap<>();
}
return metadata;
}
/**
* indicate that loading process for this tile has started
*/
public void initLoading() {
error = false;
loading = true;
}
/**
* indicate that loading process for this tile has ended
*/
public void finishLoading() {
loading = false;
loaded = true;
}
/**
*
* @return TileSource from which this tile comes
*/
public TileSource getTileSource() {
return source;
}
/**
* indicate that loading process for this tile has been canceled
*/
public void loadingCanceled() {
loading = false;
loaded = false;
}
}