001    package org.openstreetmap.gui.jmapviewer;
002    
003    //License: GPL. Copyright 2008 by Jan Peter Stotz
004    
005    import java.awt.Graphics;
006    import java.awt.Graphics2D;
007    import java.awt.geom.AffineTransform;
008    import java.awt.image.BufferedImage;
009    import java.io.IOException;
010    import java.io.InputStream;
011    import java.util.HashMap;
012    import java.util.Map;
013    
014    import javax.imageio.ImageIO;
015    
016    import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
017    import 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     */
025    public 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    }