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