001// License: GPL. For details, see Readme.txt file. 002package org.openstreetmap.gui.jmapviewer; 003 004import java.awt.Graphics; 005import java.awt.Graphics2D; 006import java.awt.geom.AffineTransform; 007import java.awt.image.BufferedImage; 008import java.io.IOException; 009import java.io.InputStream; 010import java.util.HashMap; 011import java.util.Map; 012import java.util.concurrent.Callable; 013 014import javax.imageio.ImageIO; 015 016import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 017import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 018 019/** 020 * Holds one map tile. Additionally the code for loading the tile image and 021 * painting it is also included in this class. 022 * 023 * @author Jan Peter Stotz 024 */ 025public class Tile { 026 027 /** 028 * Hourglass image that is displayed until a map tile has been loaded, except for overlay sources 029 */ 030 public static final BufferedImage LOADING_IMAGE = loadImage("images/hourglass.png"); 031 032 /** 033 * Red cross image that is displayed after a loading error, except for overlay sources 034 */ 035 public static final BufferedImage ERROR_IMAGE = loadImage("images/error.png"); 036 037 protected TileSource source; 038 protected int xtile; 039 protected int ytile; 040 protected int zoom; 041 protected BufferedImage image; 042 protected String key; 043 protected volatile boolean loaded; // field accessed by multiple threads without any monitors, needs to be volatile 044 protected volatile boolean loading; 045 protected volatile boolean error; 046 protected String error_message; 047 048 /** TileLoader-specific tile metadata */ 049 protected Map<String, String> metadata; 050 051 /** 052 * Creates a tile with empty image. 053 * 054 * @param source Tile source 055 * @param xtile X coordinate 056 * @param ytile Y coordinate 057 * @param zoom Zoom level 058 */ 059 public Tile(TileSource source, int xtile, int ytile, int zoom) { 060 this(source, xtile, ytile, zoom, LOADING_IMAGE); 061 } 062 063 /** 064 * Creates a tile with specified image. 065 * 066 * @param source Tile source 067 * @param xtile X coordinate 068 * @param ytile Y coordinate 069 * @param zoom Zoom level 070 * @param image Image content 071 */ 072 public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) { 073 this.source = source; 074 this.xtile = xtile; 075 this.ytile = ytile; 076 this.zoom = zoom; 077 this.image = image; 078 this.key = getTileKey(source, xtile, ytile, zoom); 079 } 080 081 private static BufferedImage loadImage(String path) { 082 try { 083 return ImageIO.read(JMapViewer.class.getResourceAsStream(path)); 084 } catch (IOException | IllegalArgumentException ex) { 085 ex.printStackTrace(); 086 return null; 087 } 088 } 089 090 private static class CachedCallable<V> implements Callable<V> { 091 private V result; 092 private Callable<V> callable; 093 094 /** 095 * Wraps callable so it is evaluated only once 096 * @param callable to cache 097 */ 098 CachedCallable(Callable<V> callable) { 099 this.callable = callable; 100 } 101 102 @Override 103 public synchronized V call() { 104 try { 105 if (result == null) { 106 result = callable.call(); 107 } 108 return result; 109 } catch (Exception e) { 110 // this should not happen here 111 throw new RuntimeException(e); 112 } 113 } 114 } 115 116 /** 117 * Tries to get tiles of a lower or higher zoom level (one or two level 118 * difference) from cache and use it as a placeholder until the tile has been loaded. 119 * @param cache Tile cache 120 */ 121 public void loadPlaceholderFromCache(TileCache cache) { 122 /* 123 * use LazyTask as creation of BufferedImage is very expensive 124 * this way we can avoid object creation until we're sure it's needed 125 */ 126 final CachedCallable<BufferedImage> tmpImage = new CachedCallable<>(new Callable<BufferedImage>() { 127 @Override 128 public BufferedImage call() throws Exception { 129 return new BufferedImage(source.getTileSize(), source.getTileSize(), BufferedImage.TYPE_INT_ARGB); 130 } 131 }); 132 133 for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) { 134 // first we check if there are already the 2^x tiles 135 // of a higher detail level 136 int zoomHigh = zoom + zoomDiff; 137 if (zoomDiff < 3 && zoomHigh <= JMapViewer.MAX_ZOOM) { 138 int factor = 1 << zoomDiff; 139 int xtileHigh = xtile << zoomDiff; 140 int ytileHigh = ytile << zoomDiff; 141 final double scale = 1.0 / factor; 142 143 /* 144 * use LazyTask for graphics to avoid evaluation of tmpImage, until we have 145 * something to draw 146 */ 147 CachedCallable<Graphics2D> graphics = new CachedCallable<>(new Callable<Graphics2D>() { 148 @Override 149 public Graphics2D call() throws Exception { 150 Graphics2D g = (Graphics2D) tmpImage.call().getGraphics(); 151 g.setTransform(AffineTransform.getScaleInstance(scale, scale)); 152 return g; 153 } 154 }); 155 156 int paintedTileCount = 0; 157 for (int x = 0; x < factor; x++) { 158 for (int y = 0; y < factor; y++) { 159 Tile tile = cache.getTile(source, xtileHigh + x, ytileHigh + y, zoomHigh); 160 if (tile != null && tile.isLoaded()) { 161 paintedTileCount++; 162 tile.paint(graphics.call(), x * source.getTileSize(), y * source.getTileSize()); 163 } 164 } 165 } 166 if (paintedTileCount == factor * factor) { 167 image = tmpImage.call(); 168 return; 169 } 170 } 171 172 int zoomLow = zoom - zoomDiff; 173 if (zoomLow >= JMapViewer.MIN_ZOOM) { 174 int xtileLow = xtile >> zoomDiff; 175 int ytileLow = ytile >> zoomDiff; 176 final int factor = 1 << zoomDiff; 177 final double scale = factor; 178 CachedCallable<Graphics2D> graphics = new CachedCallable<>(new Callable<Graphics2D>() { 179 @Override 180 public Graphics2D call() throws Exception { 181 Graphics2D g = (Graphics2D) tmpImage.call().getGraphics(); 182 AffineTransform at = new AffineTransform(); 183 int translateX = (xtile % factor) * source.getTileSize(); 184 int translateY = (ytile % factor) * source.getTileSize(); 185 at.setTransform(scale, 0, 0, scale, -translateX, -translateY); 186 g.setTransform(at); 187 return g; 188 } 189 190 }); 191 192 Tile tile = cache.getTile(source, xtileLow, ytileLow, zoomLow); 193 if (tile != null && tile.isLoaded()) { 194 tile.paint(graphics.call(), 0, 0); 195 image = tmpImage.call(); 196 return; 197 } 198 } 199 } 200 } 201 202 public TileSource getSource() { 203 return source; 204 } 205 206 /** 207 * Returns the X coordinate. 208 * @return tile number on the x axis of this tile 209 */ 210 public int getXtile() { 211 return xtile; 212 } 213 214 /** 215 * Returns the Y coordinate. 216 * @return tile number on the y axis of this tile 217 */ 218 public int getYtile() { 219 return ytile; 220 } 221 222 /** 223 * Returns the zoom level. 224 * @return zoom level of this tile 225 */ 226 public int getZoom() { 227 return zoom; 228 } 229 230 /** 231 * @return tile indexes of the top left corner as TileXY object 232 */ 233 public TileXY getTileXY() { 234 return new TileXY(xtile, ytile); 235 } 236 237 public BufferedImage getImage() { 238 return image; 239 } 240 241 public void setImage(BufferedImage image) { 242 this.image = image; 243 } 244 245 public void loadImage(InputStream input) throws IOException { 246 setImage(ImageIO.read(input)); 247 } 248 249 /** 250 * @return key that identifies a tile 251 */ 252 public String getKey() { 253 return key; 254 } 255 256 public boolean isLoaded() { 257 return loaded; 258 } 259 260 public boolean isLoading() { 261 return loading; 262 } 263 264 public void setLoaded(boolean loaded) { 265 this.loaded = loaded; 266 } 267 268 public String getUrl() throws IOException { 269 return source.getTileUrl(zoom, xtile, ytile); 270 } 271 272 /** 273 * Paints the tile-image on the {@link Graphics} <code>g</code> at the 274 * position <code>x</code>/<code>y</code>. 275 * 276 * @param g the Graphics object 277 * @param x x-coordinate in <code>g</code> 278 * @param y y-coordinate in <code>g</code> 279 */ 280 public void paint(Graphics g, int x, int y) { 281 if (image == null) 282 return; 283 g.drawImage(image, x, y, null); 284 } 285 286 /** 287 * Paints the tile-image on the {@link Graphics} <code>g</code> at the 288 * position <code>x</code>/<code>y</code>. 289 * 290 * @param g the Graphics object 291 * @param x x-coordinate in <code>g</code> 292 * @param y y-coordinate in <code>g</code> 293 * @param width width that tile should have 294 * @param height height that tile should have 295 */ 296 public void paint(Graphics g, int x, int y, int width, int height) { 297 if (image == null) 298 return; 299 g.drawImage(image, x, y, width, height, null); 300 } 301 302 @Override 303 public String toString() { 304 StringBuilder sb = new StringBuilder(35).append("Tile ").append(key); 305 if (loading) { 306 sb.append(" [LOADING...]"); 307 } 308 if (loaded) { 309 sb.append(" [loaded]"); 310 } 311 if (error) { 312 sb.append(" [ERROR]"); 313 } 314 return sb.toString(); 315 } 316 317 /** 318 * Note that the hash code does not include the {@link #source}. 319 * Therefore a hash based collection can only contain tiles 320 * of one {@link #source}. 321 */ 322 @Override 323 public int hashCode() { 324 final int prime = 31; 325 int result = 1; 326 result = prime * result + xtile; 327 result = prime * result + ytile; 328 result = prime * result + zoom; 329 return result; 330 } 331 332 /** 333 * Compares this object with <code>obj</code> based on 334 * the fields {@link #xtile}, {@link #ytile} and 335 * {@link #zoom}. 336 * The {@link #source} field is ignored. 337 */ 338 @Override 339 public boolean equals(Object obj) { 340 if (this == obj) 341 return true; 342 if (obj == null) 343 return false; 344 if (getClass() != obj.getClass()) 345 return false; 346 Tile other = (Tile) obj; 347 if (xtile != other.xtile) 348 return false; 349 if (ytile != other.ytile) 350 return false; 351 if (zoom != other.zoom) 352 return false; 353 return getTileSource().equals(other.getTileSource()); 354 } 355 356 public static String getTileKey(TileSource source, int xtile, int ytile, int zoom) { 357 return zoom + "/" + xtile + "/" + ytile + "@" + source.getName(); 358 } 359 360 public String getStatus() { 361 if (this.error) 362 return "error"; 363 if (this.loaded) 364 return "loaded"; 365 if (this.loading) 366 return "loading"; 367 return "new"; 368 } 369 370 public boolean hasError() { 371 return error; 372 } 373 374 public String getErrorMessage() { 375 return error_message; 376 } 377 378 public void setError(Exception e) { 379 setError(e.toString()); 380 } 381 382 public void setError(String message) { 383 error = true; 384 setImage(ERROR_IMAGE); 385 error_message = message; 386 } 387 388 /** 389 * Puts the given key/value pair to the metadata of the tile. 390 * If value is null, the (possibly existing) key/value pair is removed from 391 * the meta data. 392 * 393 * @param key Key 394 * @param value Value 395 */ 396 public void putValue(String key, String value) { 397 if (value == null || value.isEmpty()) { 398 if (metadata != null) { 399 metadata.remove(key); 400 } 401 return; 402 } 403 if (metadata == null) { 404 metadata = new HashMap<>(); 405 } 406 metadata.put(key, value); 407 } 408 409 /** 410 * returns the metadata of the Tile 411 * 412 * @param key metadata key that should be returned 413 * @return null if no such metadata exists, or the value of the metadata 414 */ 415 public String getValue(String key) { 416 if (metadata == null) return null; 417 return metadata.get(key); 418 } 419 420 /** 421 * 422 * @return metadata of the tile 423 */ 424 public Map<String, String> getMetadata() { 425 if (metadata == null) { 426 metadata = new HashMap<>(); 427 } 428 return metadata; 429 } 430 431 /** 432 * indicate that loading process for this tile has started 433 */ 434 public void initLoading() { 435 error = false; 436 loading = true; 437 } 438 439 /** 440 * indicate that loading process for this tile has ended 441 */ 442 public void finishLoading() { 443 loading = false; 444 loaded = true; 445 } 446 447 /** 448 * 449 * @return TileSource from which this tile comes 450 */ 451 public TileSource getTileSource() { 452 return source; 453 } 454 455 /** 456 * indicate that loading process for this tile has been canceled 457 */ 458 public void loadingCanceled() { 459 loading = false; 460 loaded = false; 461 } 462 463}