001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Font;
008import java.awt.Graphics;
009import java.awt.Image;
010import java.awt.Transparency;
011import java.awt.image.BufferedImage;
012import java.io.IOException;
013import java.io.ObjectInputStream;
014import java.io.ObjectOutputStream;
015import java.io.Serializable;
016import java.lang.ref.SoftReference;
017
018import javax.imageio.ImageIO;
019
020import org.openstreetmap.josm.data.coor.EastNorth;
021import org.openstreetmap.josm.gui.NavigatableComponent;
022import org.openstreetmap.josm.gui.layer.ImageryLayer;
023import org.openstreetmap.josm.gui.layer.WMSLayer;
024import org.openstreetmap.josm.tools.ImageProvider;
025
026public class GeorefImage implements Serializable {
027    private static final long serialVersionUID = 1L;
028
029    public enum State { IMAGE, NOT_IN_CACHE, FAILED, PARTLY_IN_CACHE}
030
031    private WMSLayer layer;
032    private State state;
033
034    private BufferedImage image;
035    private SoftReference<BufferedImage> reImg;
036    private int xIndex;
037    private int yIndex;
038
039    private static final Color transparentColor = new Color(0,0,0,0);
040    private Color fadeColor = transparentColor;
041
042    public EastNorth getMin() {
043        return layer.getEastNorth(xIndex, yIndex);
044    }
045
046    public EastNorth getMax() {
047        return layer.getEastNorth(xIndex+1, yIndex+1);
048    }
049
050    public GeorefImage(WMSLayer layer) {
051        this.layer = layer;
052    }
053
054    public void changePosition(int xIndex, int yIndex) {
055        if (!equalPosition(xIndex, yIndex)) {
056            this.xIndex = xIndex;
057            this.yIndex = yIndex;
058            this.image = null;
059            flushResizedCachedInstance();
060        }
061    }
062
063    public boolean equalPosition(int xIndex, int yIndex) {
064        return this.xIndex == xIndex && this.yIndex == yIndex;
065    }
066
067    /**
068     * Resets this image to initial state and release all resources being used.
069     * @since 7132
070     */
071    public void resetImage() {
072        if (image != null) {
073            image.flush();
074        }
075        changeImage(null, null);
076    }
077
078    public void changeImage(State state, BufferedImage image) {
079        flushResizedCachedInstance();
080        this.image = image;
081        this.state = state;
082        if (state == null)
083            return;
084        switch (state) {
085        case FAILED:
086            BufferedImage imgFailed = createImage();
087            layer.drawErrorTile(imgFailed);
088            this.image = imgFailed;
089            break;
090        case NOT_IN_CACHE:
091            BufferedImage img = createImage();
092            Graphics g = img.getGraphics();
093            g.setColor(Color.GRAY);
094            g.fillRect(0, 0, img.getWidth(), img.getHeight());
095            Font font = g.getFont();
096            Font tempFont = font.deriveFont(Font.PLAIN).deriveFont(36.0f);
097            g.setFont(tempFont);
098            g.setColor(Color.BLACK);
099            String text = tr("Not in cache");
100            g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, img.getHeight()/2);
101            g.setFont(font);
102            this.image = img;
103            break;
104        default:
105            if (this.image != null) {
106                this.image = layer.sharpenImage(this.image);
107            }
108            break;
109        }
110    }
111
112    private BufferedImage createImage() {
113        return new BufferedImage(layer.getImageSize(), layer.getImageSize(), BufferedImage.TYPE_INT_RGB);
114    }
115
116    public boolean paint(Graphics g, NavigatableComponent nc, int xIndex, int yIndex, int leftEdge, int bottomEdge) {
117        if (getImage() == null)
118            return false;
119
120        if(!(this.xIndex == xIndex && this.yIndex == yIndex))
121            return false;
122
123        int left = layer.getImageX(xIndex);
124        int bottom = layer.getImageY(yIndex);
125        int width = layer.getImageWidth(xIndex);
126        int height = layer.getImageHeight(yIndex);
127
128        int x = left - leftEdge;
129        int y = nc.getHeight() - (bottom - bottomEdge) - height;
130
131        // This happens if you zoom outside the world
132        if(width == 0 || height == 0)
133            return false;
134
135        // TODO: implement per-layer fade color
136        Color newFadeColor;
137        if (ImageryLayer.PROP_FADE_AMOUNT.get() == 0) {
138            newFadeColor = transparentColor;
139        } else {
140            newFadeColor = ImageryLayer.getFadeColorWithAlpha();
141        }
142
143        BufferedImage img = reImg == null?null:reImg.get();
144        if(img != null && img.getWidth() == width && img.getHeight() == height && fadeColor.equals(newFadeColor)) {
145            g.drawImage(img, x, y, null);
146            return true;
147        }
148
149        fadeColor = newFadeColor;
150
151        boolean alphaChannel = WMSLayer.PROP_ALPHA_CHANNEL.get() && getImage().getTransparency() != Transparency.OPAQUE;
152
153        try {
154            if(img != null) {
155                img.flush();
156            }
157            long freeMem = Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory();
158            // Notice that this value can get negative due to integer overflows
159
160            int multipl = alphaChannel ? 4 : 3;
161            // This happens when requesting images while zoomed out and then zooming in
162            // Storing images this large in memory will certainly hang up JOSM. Luckily
163            // traditional rendering is as fast at these zoom levels, so it's no loss.
164            // Also prevent caching if we're out of memory soon
165            if(width > 2000 || height > 2000 || width*height*multipl > freeMem) {
166                fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
167            } else {
168                // We haven't got a saved resized copy, so resize and cache it
169                img = new BufferedImage(width, height, alphaChannel?BufferedImage.TYPE_INT_ARGB:BufferedImage.TYPE_3BYTE_BGR);
170                img.getGraphics().drawImage(getImage(),
171                        0, 0, width, height, // dest
172                        0, 0, getImage().getWidth(null), getImage().getHeight(null), // src
173                        null);
174                if (!alphaChannel) {
175                    drawFadeRect(img.getGraphics(), 0, 0, width, height);
176                }
177                img.getGraphics().dispose();
178                g.drawImage(img, x, y, null);
179                reImg = new SoftReference<>(img);
180            }
181        } catch(Exception e) {
182            fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
183        }
184        return true;
185    }
186
187    private void fallbackDraw(Graphics g, Image img, int x, int y, int width, int height, boolean alphaChannel) {
188        flushResizedCachedInstance();
189        g.drawImage(
190                img, x, y, x + width, y + height,
191                0, 0, img.getWidth(null), img.getHeight(null),
192                null);
193        if (!alphaChannel) { //FIXME: fading for layers with alpha channel currently is not supported
194            drawFadeRect(g, x, y, width, height);
195        }
196    }
197
198    private void drawFadeRect(Graphics g, int x, int y, int width, int height) {
199        if (fadeColor != transparentColor) {
200            g.setColor(fadeColor);
201            g.fillRect(x, y, width, height);
202        }
203    }
204
205    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
206        state = (State) in.readObject();
207        boolean hasImage = in.readBoolean();
208        if (hasImage) {
209            image = ImageProvider.read(ImageIO.createImageInputStream(in), true, WMSLayer.PROP_ALPHA_CHANNEL.get());
210        } else {
211            in.readObject(); // read null from input stream
212            image = null;
213        }
214    }
215
216    private void writeObject(ObjectOutputStream out) throws IOException {
217        out.writeObject(state);
218        if(getImage() == null) {
219            out.writeBoolean(false);
220            out.writeObject(null);
221        } else {
222            out.writeBoolean(true);
223            ImageIO.write(getImage(), "png", ImageIO.createImageOutputStream(out));
224        }
225    }
226
227    public void flushResizedCachedInstance() {
228        if (reImg != null) {
229            BufferedImage img = reImg.get();
230            if (img != null) {
231                img.flush();
232            }
233        }
234        reImg = null;
235    }
236
237    public BufferedImage getImage() {
238        return image;
239    }
240
241    public State getState() {
242        return state;
243    }
244
245    public int getXIndex() {
246        return xIndex;
247    }
248
249    public int getYIndex() {
250        return yIndex;
251    }
252
253    public void setLayer(WMSLayer layer) {
254        this.layer = layer;
255    }
256}