001package org.openstreetmap.gui.jmapviewer;
002
003//License: GPL. Copyright 2008 by Jan Peter Stotz
004
005import java.awt.Graphics;
006import java.awt.Graphics2D;
007import java.awt.geom.AffineTransform;
008import java.awt.image.BufferedImage;
009import java.io.IOException;
010import java.io.InputStream;
011import java.util.HashMap;
012import java.util.Map;
013
014import javax.imageio.ImageIO;
015
016import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
017import 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 */
025public 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
314    public void initLoading() {
315        loaded = false;
316        error = false;
317        loading = true;
318    }
319
320    public void finishLoading() {
321        loading = false;
322        loaded = true;
323    }
324}