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