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, null); 076 } 077 078 public void changeImage(State state, BufferedImage image, String errorMsg) { 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, errorMsg); 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}