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 }