1 | // License: GPL. For details, see Readme.txt file.
|
---|
2 | package org.openstreetmap.gui.jmapviewer;
|
---|
3 |
|
---|
4 | import java.awt.Graphics;
|
---|
5 | import java.awt.Graphics2D;
|
---|
6 | import java.awt.geom.AffineTransform;
|
---|
7 | import java.awt.image.BufferedImage;
|
---|
8 | import java.io.IOException;
|
---|
9 | import java.io.InputStream;
|
---|
10 | import java.util.HashMap;
|
---|
11 | import java.util.Map;
|
---|
12 | import java.util.Objects;
|
---|
13 | import java.util.concurrent.Callable;
|
---|
14 |
|
---|
15 | import javax.imageio.ImageIO;
|
---|
16 |
|
---|
17 | import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
|
---|
18 | import 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 | */
|
---|
26 | public 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 | }
|
---|