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, except for overlay sources
028     */
029    public static BufferedImage LOADING_IMAGE;
030
031    /**
032     * Red cross image that is displayed after a loading error, except for overlay sources
033     */
034    public static BufferedImage ERROR_IMAGE;
035
036    static {
037        try {
038            LOADING_IMAGE = ImageIO.read(JMapViewer.class.getResourceAsStream("images/hourglass.png"));
039            ERROR_IMAGE = ImageIO.read(JMapViewer.class.getResourceAsStream("images/error.png"));
040        } catch (Exception ex) {
041            ex.printStackTrace();
042        }
043    }
044
045    protected TileSource source;
046    protected int xtile;
047    protected int ytile;
048    protected int zoom;
049    protected BufferedImage image;
050    protected String key;
051    protected boolean loaded = false;
052    protected boolean loading = false;
053    protected boolean error = false;
054    protected String error_message;
055
056    /** TileLoader-specific tile metadata */
057    protected Map<String, String> metadata;
058
059    /**
060     * Creates a tile with empty image.
061     *
062     * @param source Tile source
063     * @param xtile X coordinate
064     * @param ytile Y coordinate
065     * @param zoom Zoom level
066     */
067    public Tile(TileSource source, int xtile, int ytile, int zoom) {
068        this(source, xtile, ytile, zoom, LOADING_IMAGE);
069    }
070
071    /**
072     * Creates a tile with specified image.
073     *
074     * @param source Tile source
075     * @param xtile X coordinate
076     * @param ytile Y coordinate
077     * @param zoom Zoom level
078     * @param image Image content
079     */
080    public Tile(TileSource source, int xtile, int ytile, int zoom, BufferedImage image) {
081        this.source = source;
082        this.xtile = xtile;
083        this.ytile = ytile;
084        this.zoom = zoom;
085        this.image = image;
086        this.key = getTileKey(source, xtile, ytile, zoom);
087    }
088
089    /**
090     * Tries to get tiles of a lower or higher zoom level (one or two level
091     * difference) from cache and use it as a placeholder until the tile has
092     * been loaded.
093     */
094    public void loadPlaceholderFromCache(TileCache cache) {
095        BufferedImage tmpImage = new BufferedImage(source.getTileSize(), source.getTileSize(), BufferedImage.TYPE_INT_RGB);
096        Graphics2D g = (Graphics2D) tmpImage.getGraphics();
097        // g.drawImage(image, 0, 0, null);
098        for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) {
099            // first we check if there are already the 2^x tiles
100            // of a higher detail level
101            int zoom_high = zoom + zoomDiff;
102            if (zoomDiff < 3 && zoom_high <= JMapViewer.MAX_ZOOM) {
103                int factor = 1 << zoomDiff;
104                int xtile_high = xtile << zoomDiff;
105                int ytile_high = ytile << zoomDiff;
106                double scale = 1.0 / factor;
107                g.setTransform(AffineTransform.getScaleInstance(scale, scale));
108                int paintedTileCount = 0;
109                for (int x = 0; x < factor; x++) {
110                    for (int y = 0; y < factor; y++) {
111                        Tile tile = cache.getTile(source, xtile_high + x, ytile_high + y, zoom_high);
112                        if (tile != null && tile.isLoaded()) {
113                            paintedTileCount++;
114                            tile.paint(g, x * source.getTileSize(), y * source.getTileSize());
115                        }
116                    }
117                }
118                if (paintedTileCount == factor * factor) {
119                    image = tmpImage;
120                    return;
121                }
122            }
123
124            int zoom_low = zoom - zoomDiff;
125            if (zoom_low >= JMapViewer.MIN_ZOOM) {
126                int xtile_low = xtile >> zoomDiff;
127                int ytile_low = ytile >> zoomDiff;
128                int factor = (1 << zoomDiff);
129                double scale = factor;
130                AffineTransform at = new AffineTransform();
131                int translate_x = (xtile % factor) * source.getTileSize();
132                int translate_y = (ytile % factor) * source.getTileSize();
133                at.setTransform(scale, 0, 0, scale, -translate_x, -translate_y);
134                g.setTransform(at);
135                Tile tile = cache.getTile(source, xtile_low, ytile_low, zoom_low);
136                if (tile != null && tile.isLoaded()) {
137                    tile.paint(g, 0, 0);
138                    image = tmpImage;
139                    return;
140                }
141            }
142        }
143    }
144
145    public TileSource getSource() {
146        return source;
147    }
148
149    /**
150     * Returns the X coordinate.
151     * @return tile number on the x axis of this tile
152     */
153    public int getXtile() {
154        return xtile;
155    }
156
157    /**
158     * Returns the Y coordinate.
159     * @return tile number on the y axis of this tile
160     */
161    public int getYtile() {
162        return ytile;
163    }
164
165    /**
166     * Returns the zoom level.
167     * @return zoom level of this tile
168     */
169    public int getZoom() {
170        return zoom;
171    }
172
173    public BufferedImage getImage() {
174        return image;
175    }
176
177    public void setImage(BufferedImage image) {
178        this.image = image;
179    }
180
181    public void loadImage(InputStream input) throws IOException {
182        image = ImageIO.read(input);
183    }
184
185    /**
186     * @return key that identifies a tile
187     */
188    public String getKey() {
189        return key;
190    }
191
192    public boolean isLoaded() {
193        return loaded;
194    }
195
196    public boolean isLoading() {
197        return loading;
198    }
199
200    public void setLoaded(boolean loaded) {
201        this.loaded = loaded;
202    }
203
204    public String getUrl() throws IOException {
205        return source.getTileUrl(zoom, xtile, ytile);
206    }
207
208    /**
209     * Paints the tile-image on the {@link Graphics} <code>g</code> at the
210     * position <code>x</code>/<code>y</code>.
211     *
212     * @param g the Graphics object
213     * @param x x-coordinate in <code>g</code>
214     * @param y y-coordinate in <code>g</code>
215     */
216    public void paint(Graphics g, int x, int y) {
217        if (image == null)
218            return;
219        g.drawImage(image, x, y, null);
220    }
221
222    @Override
223    public String toString() {
224        return "Tile " + key;
225    }
226
227    /**
228     * Note that the hash code does not include the {@link #source}.
229     * Therefore a hash based collection can only contain tiles
230     * of one {@link #source}.
231     */
232    @Override
233    public int hashCode() {
234        final int prime = 31;
235        int result = 1;
236        result = prime * result + xtile;
237        result = prime * result + ytile;
238        result = prime * result + zoom;
239        return result;
240    }
241
242    /**
243     * Compares this object with <code>obj</code> based on
244     * the fields {@link #xtile}, {@link #ytile} and
245     * {@link #zoom}.
246     * The {@link #source} field is ignored.
247     */
248    @Override
249    public boolean equals(Object obj) {
250        if (this == obj)
251            return true;
252        if (obj == null)
253            return false;
254        if (getClass() != obj.getClass())
255            return false;
256        Tile other = (Tile) obj;
257        if (xtile != other.xtile)
258            return false;
259        if (ytile != other.ytile)
260            return false;
261        if (zoom != other.zoom)
262            return false;
263        return true;
264    }
265
266    public static String getTileKey(TileSource source, int xtile, int ytile, int zoom) {
267        return zoom + "/" + xtile + "/" + ytile + "@" + source.getName();
268    }
269
270    public String getStatus() {
271        if (this.error)
272            return "error";
273        if (this.loaded)
274            return "loaded";
275        if (this.loading)
276            return "loading";
277        return "new";
278    }
279
280    public boolean hasError() {
281        return error;
282    }
283
284    public String getErrorMessage() {
285        return error_message;
286    }
287
288    public void setError(String message) {
289        error = true;
290        setImage(ERROR_IMAGE);
291        error_message = message;
292    }
293
294    /**
295     * Puts the given key/value pair to the metadata of the tile.
296     * If value is null, the (possibly existing) key/value pair is removed from
297     * the meta data.
298     *
299     * @param key Key
300     * @param value Value
301     */
302    public void putValue(String key, String value) {
303        if (value == null || value.isEmpty()) {
304            if (metadata != null) {
305                metadata.remove(key);
306            }
307            return;
308        }
309        if (metadata == null) {
310            metadata = new HashMap<>();
311        }
312        metadata.put(key, value);
313    }
314
315    public String getValue(String key) {
316        if (metadata == null) return null;
317        return metadata.get(key);
318    }
319
320    public Map<String,String> getMetadata() {
321        return metadata;
322    }
323
324    public void initLoading() {
325        loaded = false;
326        error = false;
327        loading = true;
328    }
329
330    public void finishLoading() {
331        loading = false;
332        loaded = true;
333    }
334}