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.gui.MainApplication;
015import org.openstreetmap.josm.gui.MapView;
016import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
017import org.openstreetmap.josm.gui.mappaint.StyleSource;
018import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.BoxProvider;
019import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.BoxProviderResult;
020import org.openstreetmap.josm.gui.util.GuiHelper;
021import org.openstreetmap.josm.tools.ImageProvider;
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 Image img;
035
036    /**
037     * The alpha (opacity) value of the image. It is multiplied to the image alpha channel.
038     * Range: 0...255
039     */
040    public int alpha = 255;
041    /**
042     * The name of the image that should be displayed. It is given to the {@link ImageProvider}
043     */
044    public String name;
045    /**
046     * The StyleSource that registered the image
047     */
048    public StyleSource source;
049    /**
050     * A flag indicating that the image should automatically be scaled to the right size.
051     */
052    public boolean autoRescale;
053    /**
054     * The width of the image, as set by MapCSS
055     */
056    public int width = -1;
057    /**
058     * The height of the image, as set by MapCSS
059     */
060    public int height = -1;
061    /**
062     * The x offset of the anchor of this image
063     */
064    public int offsetX;
065    /**
066     * The y offset of the anchor of this image
067     */
068    public int offsetY;
069
070    private boolean temporary;
071
072    /**
073     * A cache that holds a disabled (gray) version of this image
074     */
075    private BufferedImage disabledImgCache;
076
077    /**
078     * Creates a new {@link MapImage}
079     * @param name The image name
080     * @param source The style source that requests this image
081     */
082    public MapImage(String name, StyleSource source) {
083        this(name, source, true);
084    }
085
086    /**
087     * Creates a new {@link MapImage}
088     * @param name The image name
089     * @param source The style source that requests this image
090     * @param autoRescale A flag indicating to automatically adjust the width/height of the image
091     */
092    public MapImage(String name, StyleSource source, boolean autoRescale) {
093        this.name = name;
094        this.source = source;
095        this.autoRescale = autoRescale;
096    }
097
098    /**
099     * Get the image associated with this MapImage object.
100     *
101     * @param disabled {@code} true to request disabled version, {@code false} for the standard version
102     * @return the image
103     */
104    public Image getImage(boolean disabled) {
105        if (disabled) {
106            return getDisabled();
107        } else {
108            return getImage();
109        }
110    }
111
112    private Image getDisabled() {
113        if (disabledImgCache != null)
114            return disabledImgCache;
115        if (img == null)
116            getImage(); // fix #7498 ?
117        Image disImg = GuiHelper.getDisabledImage(img);
118        if (disImg instanceof BufferedImage) {
119            disabledImgCache = (BufferedImage) disImg;
120        } else {
121            disabledImgCache = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
122            Graphics g = disabledImgCache.getGraphics();
123            g.drawImage(disImg, 0, 0, null);
124            g.dispose();
125        }
126        return disabledImgCache;
127    }
128
129    private Image getImage() {
130        if (img != null)
131            return img;
132        temporary = false;
133        new ImageProvider(name)
134                .setDirs(MapPaintStyles.getIconSourceDirs(source))
135                .setId("mappaint."+source.getPrefName())
136                .setArchive(source.zipIcons)
137                .setInArchiveDir(source.getZipEntryDirName())
138                .setWidth(width)
139                .setHeight(height)
140                .setOptional(true)
141                .getAsync(result -> {
142                    synchronized (this) {
143                        if (result == null) {
144                            source.logWarning(tr("Failed to locate image ''{0}''", name));
145                            ImageIcon noIcon = MapPaintStyles.getNoIconIcon(source);
146                            img = noIcon == null ? null : noIcon.getImage();
147                        } else {
148                            img = rescale(result.getImage());
149                        }
150                        if (temporary) {
151                            disabledImgCache = null;
152                            MapView mapView = MainApplication.getMap().mapView;
153                            mapView.preferenceChanged(null); // otherwise repaint is ignored, because layer hasn't changed
154                            mapView.repaint();
155                        }
156                        temporary = false;
157                    }
158                }
159        );
160        synchronized (this) {
161            if (img == null) {
162                img = ImageProvider.get("clock").getImage();
163                temporary = true;
164            }
165        }
166        return img;
167    }
168
169    /**
170     * Gets the image width
171     * @return The real image width
172     */
173    public int getWidth() {
174        return getImage().getWidth(null);
175    }
176
177    /**
178     * Gets the image height
179     * @return The real image height
180     */
181    public int getHeight() {
182        return getImage().getHeight(null);
183    }
184
185    /**
186     * Gets the alpha value the image should be multiplied with
187     * @return The value in range 0..1
188     */
189    public float getAlphaFloat() {
190        return Utils.colorInt2float(alpha);
191    }
192
193    /**
194     * Determines if image is not completely loaded and {@code getImage()} returns a temporary image.
195     * @return {@code true} if image is not completely loaded and getImage() returns a temporary image
196     */
197    public boolean isTemporary() {
198        return temporary;
199    }
200
201    protected class MapImageBoxProvider implements BoxProvider {
202        @Override
203        public BoxProviderResult get() {
204            return new BoxProviderResult(box(), temporary);
205        }
206
207        private Rectangle box() {
208            int w = getWidth(), h = getHeight();
209            if (mustRescale(getImage())) {
210                w = 16;
211                h = 16;
212            }
213            return new Rectangle(-w/2, -h/2, w, h);
214        }
215
216        private MapImage getParent() {
217            return MapImage.this;
218        }
219
220        @Override
221        public int hashCode() {
222            return MapImage.this.hashCode();
223        }
224
225        @Override
226        public boolean equals(Object obj) {
227            if (!(obj instanceof BoxProvider))
228                return false;
229            if (obj instanceof MapImageBoxProvider) {
230                MapImageBoxProvider other = (MapImageBoxProvider) obj;
231                return MapImage.this.equals(other.getParent());
232            } else if (temporary) {
233                return false;
234            } else {
235                final BoxProvider other = (BoxProvider) obj;
236                BoxProviderResult resultOther = other.get();
237                if (resultOther.isTemporary()) return false;
238                return box().equals(resultOther.getBox());
239            }
240        }
241    }
242
243    /**
244     * Gets a box provider that provides a box that covers the size of this image
245     * @return The box provider
246     */
247    public BoxProvider getBoxProvider() {
248        return new MapImageBoxProvider();
249    }
250
251    /**
252     * Rescale excessively large images.
253     * @param image the unscaled image
254     * @return The scaled down version to 16x16 pixels if the image height and width exceeds 48 pixels and no size has been explicitly specified
255     */
256    private Image rescale(Image image) {
257        if (image == null) return null;
258        // Scale down large (.svg) images to 16x16 pixels if no size is explicitly specified
259        if (mustRescale(image)) {
260            return ImageProvider.createBoundedImage(image, 16);
261        } else {
262            return image;
263        }
264    }
265
266    private boolean mustRescale(Image image) {
267        return autoRescale && width == -1 && image.getWidth(null) > MAX_SIZE
268             && height == -1 && image.getHeight(null) > MAX_SIZE;
269    }
270
271    @Override
272    public boolean equals(Object obj) {
273        if (this == obj) return true;
274        if (obj == null || getClass() != obj.getClass()) return false;
275        MapImage mapImage = (MapImage) obj;
276        return alpha == mapImage.alpha &&
277                autoRescale == mapImage.autoRescale &&
278                width == mapImage.width &&
279                height == mapImage.height &&
280                Objects.equals(name, mapImage.name) &&
281                Objects.equals(source, mapImage.source);
282    }
283
284    @Override
285    public int hashCode() {
286        return Objects.hash(alpha, name, source, autoRescale, width, height);
287    }
288
289    @Override
290    public String toString() {
291        return name;
292    }
293}