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