001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint;
003
004import java.awt.Graphics;
005import java.awt.Image;
006import java.awt.Rectangle;
007import java.awt.image.BufferedImage;
008import java.util.Objects;
009
010import javax.swing.ImageIcon;
011
012import org.openstreetmap.josm.Main;
013import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.BoxProvider;
014import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.BoxProviderResult;
015import org.openstreetmap.josm.gui.util.GuiHelper;
016import org.openstreetmap.josm.tools.ImageProvider;
017import org.openstreetmap.josm.tools.ImageProvider.ImageCallback;
018import org.openstreetmap.josm.tools.Utils;
019
020/**
021 * An image that will be displayed on the map.
022 */
023public class MapImage {
024
025    private static final int MAX_SIZE = 48;
026
027    /**
028     * ImageIcon can change while the image is loading.
029     */
030    private BufferedImage img;
031
032    public int alpha = 255;
033    public String name;
034    public StyleSource source;
035    public int width = -1;
036    public int height = -1;
037    public int offsetX = 0;
038    public int offsetY = 0;
039
040    private boolean temporary;
041    private BufferedImage disabledImgCache;
042
043    public MapImage(String name, StyleSource source) {
044        this.name = name;
045        this.source = source;
046    }
047
048    /**
049     * Get the image associated with this MapImage object.
050     * 
051     * @param disabled {@code} true to request disabled version, {@code false} for the standard version
052     * @return the image
053     */
054    public BufferedImage getImage(boolean disabled) {
055        if (disabled) {
056            return getDisabled();
057        } else {
058            return getImage();
059        }
060    }
061
062    private BufferedImage getDisabled() {
063        if (disabledImgCache != null)
064                return disabledImgCache;
065        if (img == null)
066            getImage(); // fix #7498 ?
067        Image disImg = GuiHelper.getDisabledImage(img);
068        if (disImg instanceof BufferedImage) {
069            disabledImgCache = (BufferedImage) disImg;
070        } else {
071            disabledImgCache = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
072            Graphics g = disabledImgCache.getGraphics();
073            g.drawImage(disImg, 0, 0, null);
074            g.dispose();
075        }
076        return disabledImgCache;
077    }
078
079    private BufferedImage getImage() {
080        if (img != null)
081            return img;
082        temporary = false;
083        new ImageProvider(name)
084                .setDirs(MapPaintStyles.getIconSourceDirs(source))
085                .setId("mappaint."+source.getPrefName())
086                .setArchive(source.zipIcons)
087                .setInArchiveDir(source.getZipEntryDirName())
088                .setWidth(width)
089                .setHeight(height)
090                .setOptional(true)
091                .getInBackground(new ImageCallback() {
092                    @Override
093                    public void finished(ImageIcon result) {
094                        synchronized (MapImage.this) {
095                            if (result == null) {
096                                ImageIcon noIcon = MapPaintStyles.getNoIcon_Icon(source);
097                                img = noIcon == null ? null : (BufferedImage) noIcon.getImage();
098                            } else {
099                                img = (BufferedImage) rescale(result.getImage());
100                            }
101                            if (temporary) {
102                                disabledImgCache = null;
103                                Main.map.mapView.preferenceChanged(null); // otherwise repaint is ignored, because layer hasn't changed
104                                Main.map.mapView.repaint();
105                            }
106                            temporary = false;
107                        }
108                    }
109                }
110        );
111        synchronized (this) {
112            if (img == null) {
113                img = (BufferedImage) ImageProvider.get("clock").getImage();
114                temporary = true;
115            }
116        }
117        return img;
118    }
119
120    public int getWidth() {
121        return getImage().getWidth(null);
122    }
123
124    public int getHeight() {
125        return getImage().getHeight(null);
126    }
127
128    public float getAlphaFloat() {
129        return Utils.color_int2float(alpha);
130    }
131
132    /**
133     * Returns true, if image is not completely loaded and getImage() returns a temporary image.
134     */
135    public boolean isTemporary() {
136        return temporary;
137    }
138
139    protected class MapImageBoxProvider implements BoxProvider {
140        @Override
141        public BoxProviderResult get() {
142            return new BoxProviderResult(box(), temporary);
143        }
144
145        private Rectangle box() {
146            int w = getWidth(), h = getHeight();
147            if (mustRescale(getImage())) {
148                w = 16;
149                h = 16;
150            }
151            return new Rectangle(-w/2, -h/2, w, h);
152        }
153
154        private MapImage getParent() {
155            return MapImage.this;
156        }
157
158        @Override
159        public int hashCode() {
160            return MapImage.this.hashCode();
161        }
162
163        @Override
164        public boolean equals(Object obj) {
165            if (!(obj instanceof BoxProvider))
166                return false;
167            if (obj instanceof MapImageBoxProvider) {
168                MapImageBoxProvider other = (MapImageBoxProvider) obj;
169                return MapImage.this.equals(other.getParent());
170            } else if (temporary) {
171                return false;
172            } else {
173                final BoxProvider other = (BoxProvider) obj;
174                BoxProviderResult resultOther = other.get();
175                if (resultOther.isTemporary()) return false;
176                return box().equals(resultOther.getBox());
177            }
178        }
179    }
180
181    public BoxProvider getBoxProvider() {
182        return new MapImageBoxProvider();
183    }
184
185    /**
186     * Rescale excessively large images.
187     * @param image the unscaled image
188     * @return The scaled down version to 16x16 pixels if the image height and width exceeds 48 pixels and no size has been explicitely specified
189     */
190    private Image rescale(Image image) {
191        if (image == null) return null;
192        // Scale down large (.svg) images to 16x16 pixels if no size is explicitely specified
193        if (mustRescale(image)) {
194            return ImageProvider.createBoundedImage(image, 16);
195        } else {
196            return image;
197        }
198    }
199
200    private boolean mustRescale(Image image) {
201        return ((width  == -1 && image.getWidth(null) > MAX_SIZE)
202             && (height == -1 && image.getHeight(null) > MAX_SIZE));
203    }
204
205    @Override
206    public boolean equals(Object obj) {
207        if (obj == null || getClass() != obj.getClass())
208            return false;
209        final MapImage other = (MapImage) obj;
210        // img changes when image is fully loaded and can't be used for equality check.
211        return  alpha == other.alpha &&
212                Objects.equals(name, other.name) &&
213                Objects.equals(source, other.source) &&
214                width == other.width &&
215                height == other.height;
216    }
217
218    @Override
219    public int hashCode() {
220        int hash = 7;
221        hash = 67 * hash + alpha;
222        hash = 67 * hash + name.hashCode();
223        hash = 67 * hash + source.hashCode();
224        hash = 67 * hash + width;
225        hash = 67 * hash + height;
226        return hash;
227    }
228
229    @Override
230    public String toString() {
231        return name;
232    }
233}