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