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; 012 013import javax.imageio.ImageIO; 014 015import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 016import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 017 018/** 019 * Holds one map tile. Additionally the code for loading the tile image and 020 * painting it is also included in this class. 021 * 022 * @author Jan Peter Stotz 023 */ 024public class Tile { 025 026 /** 027 * Hourglass image that is displayed until a map tile has been loaded, except for overlay sources 028 */ 029 public static BufferedImage LOADING_IMAGE; 030 031 /** 032 * Red cross image that is displayed after a loading error, except for overlay sources 033 */ 034 public static BufferedImage ERROR_IMAGE; 035 036 static { 037 try { 038 LOADING_IMAGE = ImageIO.read(JMapViewer.class.getResourceAsStream("images/hourglass.png")); 039 ERROR_IMAGE = ImageIO.read(JMapViewer.class.getResourceAsStream("images/error.png")); 040 } catch (Exception ex) { 041 ex.printStackTrace(); 042 } 043 } 044 045 protected TileSource source; 046 protected int xtile; 047 protected int ytile; 048 protected int zoom; 049 protected BufferedImage image; 050 protected String key; 051 protected boolean loaded = false; 052 protected boolean loading = false; 053 protected boolean error = false; 054 protected String error_message; 055 056 /** TileLoader-specific tile metadata */ 057 protected Map<String, String> metadata; 058 059 /** 060 * Creates a tile with empty image. 061 * 062 * @param source Tile source 063 * @param xtile X coordinate 064 * @param ytile Y coordinate 065 * @param zoom Zoom level 066 */ 067 public Tile(TileSource source, int xtile, int ytile, int zoom) { 068 this(source, xtile, ytile, zoom, LOADING_IMAGE); 069 } 070 071 /** 072 * Creates a tile with specified image. 073 * 074 * @param source Tile source 075 * @param xtile X coordinate 076 * @param ytile Y coordinate 077 * @param zoom Zoom level 078 * @param image Image content 079 */ 080 public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) { 081 this.source = source; 082 this.xtile = xtile; 083 this.ytile = ytile; 084 this.zoom = zoom; 085 this.image = image; 086 this.key = getTileKey(source, xtile, ytile, zoom); 087 } 088 089 /** 090 * Tries to get tiles of a lower or higher zoom level (one or two level 091 * difference) from cache and use it as a placeholder until the tile has 092 * been loaded. 093 */ 094 public void loadPlaceholderFromCache(TileCache cache) { 095 BufferedImage tmpImage = new BufferedImage(source.getTileSize(), source.getTileSize(), BufferedImage.TYPE_INT_RGB); 096 Graphics2D g = (Graphics2D) tmpImage.getGraphics(); 097 // g.drawImage(image, 0, 0, null); 098 for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) { 099 // first we check if there are already the 2^x tiles 100 // of a higher detail level 101 int zoom_high = zoom + zoomDiff; 102 if (zoomDiff < 3 && zoom_high <= JMapViewer.MAX_ZOOM) { 103 int factor = 1 << zoomDiff; 104 int xtile_high = xtile << zoomDiff; 105 int ytile_high = ytile << zoomDiff; 106 double scale = 1.0 / factor; 107 g.setTransform(AffineTransform.getScaleInstance(scale, scale)); 108 int paintedTileCount = 0; 109 for (int x = 0; x < factor; x++) { 110 for (int y = 0; y < factor; y++) { 111 Tile tile = cache.getTile(source, xtile_high + x, ytile_high + y, zoom_high); 112 if (tile != null && tile.isLoaded()) { 113 paintedTileCount++; 114 tile.paint(g, x * source.getTileSize(), y * source.getTileSize()); 115 } 116 } 117 } 118 if (paintedTileCount == factor * factor) { 119 image = tmpImage; 120 return; 121 } 122 } 123 124 int zoom_low = zoom - zoomDiff; 125 if (zoom_low >= JMapViewer.MIN_ZOOM) { 126 int xtile_low = xtile >> zoomDiff; 127 int ytile_low = ytile >> zoomDiff; 128 int factor = (1 << zoomDiff); 129 double scale = factor; 130 AffineTransform at = new AffineTransform(); 131 int translate_x = (xtile % factor) * source.getTileSize(); 132 int translate_y = (ytile % factor) * source.getTileSize(); 133 at.setTransform(scale, 0, 0, scale, -translate_x, -translate_y); 134 g.setTransform(at); 135 Tile tile = cache.getTile(source, xtile_low, ytile_low, zoom_low); 136 if (tile != null && tile.isLoaded()) { 137 tile.paint(g, 0, 0); 138 image = tmpImage; 139 return; 140 } 141 } 142 } 143 } 144 145 public TileSource getSource() { 146 return source; 147 } 148 149 /** 150 * Returns the X coordinate. 151 * @return tile number on the x axis of this tile 152 */ 153 public int getXtile() { 154 return xtile; 155 } 156 157 /** 158 * Returns the Y coordinate. 159 * @return tile number on the y axis of this tile 160 */ 161 public int getYtile() { 162 return ytile; 163 } 164 165 /** 166 * Returns the zoom level. 167 * @return zoom level of this tile 168 */ 169 public int getZoom() { 170 return zoom; 171 } 172 173 public BufferedImage getImage() { 174 return image; 175 } 176 177 public void setImage(BufferedImage image) { 178 this.image = image; 179 } 180 181 public void loadImage(InputStream input) throws IOException { 182 image = ImageIO.read(input); 183 } 184 185 /** 186 * @return key that identifies a tile 187 */ 188 public String getKey() { 189 return key; 190 } 191 192 public boolean isLoaded() { 193 return loaded; 194 } 195 196 public boolean isLoading() { 197 return loading; 198 } 199 200 public void setLoaded(boolean loaded) { 201 this.loaded = loaded; 202 } 203 204 public String getUrl() throws IOException { 205 return source.getTileUrl(zoom, xtile, ytile); 206 } 207 208 /** 209 * Paints the tile-image on the {@link Graphics} <code>g</code> at the 210 * position <code>x</code>/<code>y</code>. 211 * 212 * @param g the Graphics object 213 * @param x x-coordinate in <code>g</code> 214 * @param y y-coordinate in <code>g</code> 215 */ 216 public void paint(Graphics g, int x, int y) { 217 if (image == null) 218 return; 219 g.drawImage(image, x, y, null); 220 } 221 222 @Override 223 public String toString() { 224 return "Tile " + key; 225 } 226 227 /** 228 * Note that the hash code does not include the {@link #source}. 229 * Therefore a hash based collection can only contain tiles 230 * of one {@link #source}. 231 */ 232 @Override 233 public int hashCode() { 234 final int prime = 31; 235 int result = 1; 236 result = prime * result + xtile; 237 result = prime * result + ytile; 238 result = prime * result + zoom; 239 return result; 240 } 241 242 /** 243 * Compares this object with <code>obj</code> based on 244 * the fields {@link #xtile}, {@link #ytile} and 245 * {@link #zoom}. 246 * The {@link #source} field is ignored. 247 */ 248 @Override 249 public boolean equals(Object obj) { 250 if (this == obj) 251 return true; 252 if (obj == null) 253 return false; 254 if (getClass() != obj.getClass()) 255 return false; 256 Tile other = (Tile) obj; 257 if (xtile != other.xtile) 258 return false; 259 if (ytile != other.ytile) 260 return false; 261 if (zoom != other.zoom) 262 return false; 263 return true; 264 } 265 266 public static String getTileKey(TileSource source, int xtile, int ytile, int zoom) { 267 return zoom + "/" + xtile + "/" + ytile + "@" + source.getName(); 268 } 269 270 public String getStatus() { 271 if (this.error) 272 return "error"; 273 if (this.loaded) 274 return "loaded"; 275 if (this.loading) 276 return "loading"; 277 return "new"; 278 } 279 280 public boolean hasError() { 281 return error; 282 } 283 284 public String getErrorMessage() { 285 return error_message; 286 } 287 288 public void setError(String message) { 289 error = true; 290 setImage(ERROR_IMAGE); 291 error_message = message; 292 } 293 294 /** 295 * Puts the given key/value pair to the metadata of the tile. 296 * If value is null, the (possibly existing) key/value pair is removed from 297 * the meta data. 298 * 299 * @param key Key 300 * @param value Value 301 */ 302 public void putValue(String key, String value) { 303 if (value == null || value.isEmpty()) { 304 if (metadata != null) { 305 metadata.remove(key); 306 } 307 return; 308 } 309 if (metadata == null) { 310 metadata = new HashMap<>(); 311 } 312 metadata.put(key, value); 313 } 314 315 public String getValue(String key) { 316 if (metadata == null) return null; 317 return metadata.get(key); 318 } 319 320 public Map<String,String> getMetadata() { 321 return metadata; 322 } 323 324 public void initLoading() { 325 loaded = false; 326 error = false; 327 loading = true; 328 } 329 330 public void finishLoading() { 331 loading = false; 332 loaded = true; 333 } 334}