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