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}