source: osm/applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/Tile.java@ 34760

Last change on this file since 34760 was 34760, checked in by donvip, 5 years ago

see #josm16937 - make sure images are loaded using JOSM bullet-proof engine

  • Property svn:eol-style set to native
File size: 13.3 KB
Line 
1// License: GPL. For details, see Readme.txt file.
2package org.openstreetmap.gui.jmapviewer;
3
4import java.awt.Graphics;
5import java.awt.Graphics2D;
6import java.awt.geom.AffineTransform;
7import java.awt.image.BufferedImage;
8import java.io.IOException;
9import java.io.InputStream;
10import java.util.HashMap;
11import java.util.Map;
12import java.util.Objects;
13import java.util.concurrent.Callable;
14
15import javax.imageio.ImageIO;
16
17import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
18import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
19
20/**
21 * Holds one map tile. Additionally the code for loading the tile image and
22 * painting it is also included in this class.
23 *
24 * @author Jan Peter Stotz
25 */
26public class Tile {
27
28 /**
29 * Hourglass image that is displayed until a map tile has been loaded, except for overlay sources
30 */
31 public static final BufferedImage LOADING_IMAGE = loadImage("images/hourglass.png");
32
33 /**
34 * Red cross image that is displayed after a loading error, except for overlay sources
35 */
36 public static final BufferedImage ERROR_IMAGE = loadImage("images/error.png");
37
38 protected TileSource source;
39 protected int xtile;
40 protected int ytile;
41 protected int zoom;
42 protected BufferedImage image;
43 protected String key;
44 protected volatile boolean loaded; // field accessed by multiple threads without any monitors, needs to be volatile
45 protected volatile boolean loading;
46 protected volatile boolean error;
47 protected String error_message;
48
49 /** TileLoader-specific tile metadata */
50 protected Map<String, String> metadata;
51
52 /**
53 * Creates a tile with empty image.
54 *
55 * @param source Tile source
56 * @param xtile X coordinate
57 * @param ytile Y coordinate
58 * @param zoom Zoom level
59 */
60 public Tile(TileSource source, int xtile, int ytile, int zoom) {
61 this(source, xtile, ytile, zoom, LOADING_IMAGE);
62 }
63
64 /**
65 * Creates a tile with specified image.
66 *
67 * @param source Tile source
68 * @param xtile X coordinate
69 * @param ytile Y coordinate
70 * @param zoom Zoom level
71 * @param image Image content
72 */
73 public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) {
74 this.source = source;
75 this.xtile = xtile;
76 this.ytile = ytile;
77 this.zoom = zoom;
78 this.image = image;
79 this.key = getTileKey(source, xtile, ytile, zoom);
80 }
81
82 private static BufferedImage loadImage(String path) {
83 try {
84 return FeatureAdapter.readImage(JMapViewer.class.getResource(path));
85 } catch (IOException | IllegalArgumentException ex) {
86 ex.printStackTrace();
87 return null;
88 }
89 }
90
91 private static class CachedCallable<V> implements Callable<V> {
92 private V result;
93 private Callable<V> callable;
94
95 /**
96 * Wraps callable so it is evaluated only once
97 * @param callable to cache
98 */
99 CachedCallable(Callable<V> callable) {
100 this.callable = callable;
101 }
102
103 @Override
104 public synchronized V call() {
105 try {
106 if (result == null) {
107 result = callable.call();
108 }
109 return result;
110 } catch (Exception e) {
111 // this should not happen here
112 throw new RuntimeException(e);
113 }
114 }
115 }
116
117 /**
118 * Tries to get tiles of a lower or higher zoom level (one or two level
119 * difference) from cache and use it as a placeholder until the tile has been loaded.
120 * @param cache Tile cache
121 */
122 public void loadPlaceholderFromCache(TileCache cache) {
123 /*
124 * use LazyTask as creation of BufferedImage is very expensive
125 * this way we can avoid object creation until we're sure it's needed
126 */
127 final CachedCallable<BufferedImage> tmpImage = new CachedCallable<>(new Callable<BufferedImage>() {
128 @Override
129 public BufferedImage call() throws Exception {
130 return new BufferedImage(source.getTileSize(), source.getTileSize(), BufferedImage.TYPE_INT_ARGB);
131 }
132 });
133
134 for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) {
135 // first we check if there are already the 2^x tiles
136 // of a higher detail level
137 int zoomHigh = zoom + zoomDiff;
138 if (zoomDiff < 3 && zoomHigh <= JMapViewer.MAX_ZOOM) {
139 int factor = 1 << zoomDiff;
140 int xtileHigh = xtile << zoomDiff;
141 int ytileHigh = ytile << zoomDiff;
142 final double scale = 1.0 / factor;
143
144 /*
145 * use LazyTask for graphics to avoid evaluation of tmpImage, until we have
146 * something to draw
147 */
148 CachedCallable<Graphics2D> graphics = new CachedCallable<>(new Callable<Graphics2D>() {
149 @Override
150 public Graphics2D call() throws Exception {
151 Graphics2D g = (Graphics2D) tmpImage.call().getGraphics();
152 g.setTransform(AffineTransform.getScaleInstance(scale, scale));
153 return g;
154 }
155 });
156
157 int paintedTileCount = 0;
158 for (int x = 0; x < factor; x++) {
159 for (int y = 0; y < factor; y++) {
160 Tile tile = cache.getTile(source, xtileHigh + x, ytileHigh + y, zoomHigh);
161 if (tile != null && tile.isLoaded()) {
162 paintedTileCount++;
163 tile.paint(graphics.call(), x * source.getTileSize(), y * source.getTileSize());
164 }
165 }
166 }
167 if (paintedTileCount == factor * factor) {
168 image = tmpImage.call();
169 return;
170 }
171 }
172
173 int zoomLow = zoom - zoomDiff;
174 if (zoomLow >= JMapViewer.MIN_ZOOM) {
175 int xtileLow = xtile >> zoomDiff;
176 int ytileLow = ytile >> zoomDiff;
177 final int factor = 1 << zoomDiff;
178 final double scale = factor;
179 CachedCallable<Graphics2D> graphics = new CachedCallable<>(new Callable<Graphics2D>() {
180 @Override
181 public Graphics2D call() throws Exception {
182 Graphics2D g = (Graphics2D) tmpImage.call().getGraphics();
183 AffineTransform at = new AffineTransform();
184 int translateX = (xtile % factor) * source.getTileSize();
185 int translateY = (ytile % factor) * source.getTileSize();
186 at.setTransform(scale, 0, 0, scale, -translateX, -translateY);
187 g.setTransform(at);
188 return g;
189 }
190
191 });
192
193 Tile tile = cache.getTile(source, xtileLow, ytileLow, zoomLow);
194 if (tile != null && tile.isLoaded()) {
195 tile.paint(graphics.call(), 0, 0);
196 image = tmpImage.call();
197 return;
198 }
199 }
200 }
201 }
202
203 public TileSource getSource() {
204 return source;
205 }
206
207 /**
208 * Returns the X coordinate.
209 * @return tile number on the x axis of this tile
210 */
211 public int getXtile() {
212 return xtile;
213 }
214
215 /**
216 * Returns the Y coordinate.
217 * @return tile number on the y axis of this tile
218 */
219 public int getYtile() {
220 return ytile;
221 }
222
223 /**
224 * Returns the zoom level.
225 * @return zoom level of this tile
226 */
227 public int getZoom() {
228 return zoom;
229 }
230
231 /**
232 * @return tile indexes of the top left corner as TileXY object
233 */
234 public TileXY getTileXY() {
235 return new TileXY(xtile, ytile);
236 }
237
238 public BufferedImage getImage() {
239 return image;
240 }
241
242 public void setImage(BufferedImage image) {
243 this.image = image;
244 }
245
246 public void loadImage(InputStream input) throws IOException {
247 setImage(ImageIO.read(input));
248 }
249
250 /**
251 * @return key that identifies a tile
252 */
253 public String getKey() {
254 return key;
255 }
256
257 public boolean isLoaded() {
258 return loaded;
259 }
260
261 public boolean isLoading() {
262 return loading;
263 }
264
265 public void setLoaded(boolean loaded) {
266 this.loaded = loaded;
267 }
268
269 public String getUrl() throws IOException {
270 return source.getTileUrl(zoom, xtile, ytile);
271 }
272
273 /**
274 * Paints the tile-image on the {@link Graphics} <code>g</code> at the
275 * position <code>x</code>/<code>y</code>.
276 *
277 * @param g the Graphics object
278 * @param x x-coordinate in <code>g</code>
279 * @param y y-coordinate in <code>g</code>
280 */
281 public void paint(Graphics g, int x, int y) {
282 if (image == null)
283 return;
284 g.drawImage(image, x, y, null);
285 }
286
287 /**
288 * Paints the tile-image on the {@link Graphics} <code>g</code> at the
289 * position <code>x</code>/<code>y</code>.
290 *
291 * @param g the Graphics object
292 * @param x x-coordinate in <code>g</code>
293 * @param y y-coordinate in <code>g</code>
294 * @param width width that tile should have
295 * @param height height that tile should have
296 */
297 public void paint(Graphics g, int x, int y, int width, int height) {
298 if (image == null)
299 return;
300 g.drawImage(image, x, y, width, height, null);
301 }
302
303 @Override
304 public String toString() {
305 StringBuilder sb = new StringBuilder(35).append("Tile ").append(key);
306 if (loading) {
307 sb.append(" [LOADING...]");
308 }
309 if (loaded) {
310 sb.append(" [loaded]");
311 }
312 if (error) {
313 sb.append(" [ERROR]");
314 }
315 return sb.toString();
316 }
317
318 /**
319 * Note that the hash code does not include the {@link #source}.
320 * Therefore a hash based collection can only contain tiles
321 * of one {@link #source}.
322 */
323 @Override
324 public int hashCode() {
325 final int prime = 31;
326 int result = 1;
327 result = prime * result + xtile;
328 result = prime * result + ytile;
329 result = prime * result + zoom;
330 return result;
331 }
332
333 /**
334 * Compares this object with <code>obj</code> based on
335 * the fields {@link #xtile}, {@link #ytile} and
336 * {@link #zoom}.
337 * The {@link #source} field is ignored.
338 */
339 @Override
340 public boolean equals(Object obj) {
341 if (this == obj)
342 return true;
343 if (obj == null || !(obj instanceof Tile))
344 return false;
345 final Tile other = (Tile) obj;
346 return xtile == other.xtile
347 && ytile == other.ytile
348 && zoom == other.zoom
349 && Objects.equals(source, other.source);
350 }
351
352 public static String getTileKey(TileSource source, int xtile, int ytile, int zoom) {
353 return zoom + "/" + xtile + "/" + ytile + "@" + source.getName();
354 }
355
356 public String getStatus() {
357 if (this.error)
358 return "error";
359 if (this.loaded)
360 return "loaded";
361 if (this.loading)
362 return "loading";
363 return "new";
364 }
365
366 public boolean hasError() {
367 return error;
368 }
369
370 public String getErrorMessage() {
371 return error_message;
372 }
373
374 public void setError(Exception e) {
375 setError(e.toString());
376 }
377
378 public void setError(String message) {
379 error = true;
380 setImage(ERROR_IMAGE);
381 error_message = message;
382 }
383
384 /**
385 * Puts the given key/value pair to the metadata of the tile.
386 * If value is null, the (possibly existing) key/value pair is removed from
387 * the meta data.
388 *
389 * @param key Key
390 * @param value Value
391 */
392 public void putValue(String key, String value) {
393 if (value == null || value.isEmpty()) {
394 if (metadata != null) {
395 metadata.remove(key);
396 }
397 return;
398 }
399 if (metadata == null) {
400 metadata = new HashMap<>();
401 }
402 metadata.put(key, value);
403 }
404
405 /**
406 * returns the metadata of the Tile
407 *
408 * @param key metadata key that should be returned
409 * @return null if no such metadata exists, or the value of the metadata
410 */
411 public String getValue(String key) {
412 if (metadata == null) return null;
413 return metadata.get(key);
414 }
415
416 /**
417 *
418 * @return metadata of the tile
419 */
420 public Map<String, String> getMetadata() {
421 if (metadata == null) {
422 metadata = new HashMap<>();
423 }
424 return metadata;
425 }
426
427 /**
428 * indicate that loading process for this tile has started
429 */
430 public void initLoading() {
431 error = false;
432 loading = true;
433 }
434
435 /**
436 * indicate that loading process for this tile has ended
437 */
438 public void finishLoading() {
439 loading = false;
440 loaded = true;
441 }
442
443 /**
444 *
445 * @return TileSource from which this tile comes
446 */
447 public TileSource getTileSource() {
448 return source;
449 }
450
451 /**
452 * indicate that loading process for this tile has been canceled
453 */
454 public void loadingCanceled() {
455 loading = false;
456 loaded = false;
457 }
458}
Note: See TracBrowser for help on using the repository browser.