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