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.Dimension;
007import java.awt.Graphics;
008import java.awt.Image;
009import java.awt.Rectangle;
010import java.awt.image.BufferedImage;
011import java.util.Objects;
012import java.util.concurrent.CompletableFuture;
013import java.util.concurrent.ExecutionException;
014import java.util.function.Consumer;
015
016import javax.swing.ImageIcon;
017
018import org.openstreetmap.josm.gui.MainApplication;
019import org.openstreetmap.josm.gui.MapView;
020import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
021import org.openstreetmap.josm.gui.mappaint.StyleSource;
022import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.BoxProvider;
023import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.BoxProviderResult;
024import org.openstreetmap.josm.gui.util.GuiHelper;
025import org.openstreetmap.josm.tools.ColorHelper;
026import org.openstreetmap.josm.tools.ImageProvider;
027import org.openstreetmap.josm.tools.ImageResource;
028import org.openstreetmap.josm.tools.Logging;
029
030/**
031 * An image that will be displayed on the map.
032 */
033public class MapImage {
034
035    private static final int MAX_SIZE = 48;
036
037    /**
038     * ImageIcon can change while the image is loading.
039     */
040    private Image img;
041    private ImageResource imageResource;
042
043    /**
044     * The alpha (opacity) value of the image. It is multiplied to the image alpha channel.
045     * Range: 0...255
046     */
047    public int alpha = 255;
048    /**
049     * The name of the image that should be displayed. It is given to the {@link ImageProvider}
050     */
051    public String name;
052    /**
053     * The StyleSource that registered the image
054     */
055    public StyleSource source;
056    /**
057     * A flag indicating that the image should automatically be scaled to the right size.
058     */
059    public boolean autoRescale;
060    /**
061     * The width of the image, as set by MapCSS
062     */
063    public int width = -1;
064    /**
065     * The height of the image, as set by MapCSS
066     */
067    public int height = -1;
068    /**
069     * The x offset of the anchor of this image
070     */
071    public int offsetX;
072    /**
073     * The y offset of the anchor of this image
074     */
075    public int offsetY;
076
077    private boolean temporary;
078
079    /**
080     * A cache that holds a disabled (gray) version of this image
081     */
082    private BufferedImage disabledImgCache;
083
084    /**
085     * Creates a new {@link MapImage}
086     * @param name The image name
087     * @param source The style source that requests this image
088     */
089    public MapImage(String name, StyleSource source) {
090        this(name, source, true);
091    }
092
093    /**
094     * Creates a new {@link MapImage}
095     * @param name The image name
096     * @param source The style source that requests this image
097     * @param autoRescale A flag indicating to automatically adjust the width/height of the image
098     */
099    public MapImage(String name, StyleSource source, boolean autoRescale) {
100        this.name = name;
101        this.source = source;
102        this.autoRescale = autoRescale;
103    }
104
105    /**
106     * Get the image associated with this MapImage object.
107     *
108     * @param disabled {@code} true to request disabled version, {@code false} for the standard version
109     * @return the image
110     */
111    public Image getImage(boolean disabled) {
112        if (disabled) {
113            return getDisabled();
114        } else {
115            return getImage();
116        }
117    }
118
119    /**
120     * Get the image resource associated with this MapImage object.
121     * This method blocks until the image resource has been loaded.
122     * @return the image resource
123     */
124    public ImageResource getImageResource() {
125        if (imageResource == null) {
126            try {
127                // load and wait for the image resource
128                loadImageResource().get();
129            } catch (ExecutionException | InterruptedException e) {
130                Logging.warn(e);
131                Thread.currentThread().interrupt();
132            }
133        }
134        return imageResource;
135    }
136
137    private Image getDisabled() {
138        if (disabledImgCache != null)
139            return disabledImgCache;
140        if (img == null)
141            getImage(); // fix #7498 ?
142        Image disImg = GuiHelper.getDisabledImage(img);
143        if (disImg instanceof BufferedImage) {
144            disabledImgCache = (BufferedImage) disImg;
145        } else {
146            disabledImgCache = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
147            Graphics g = disabledImgCache.getGraphics();
148            g.drawImage(disImg, 0, 0, null);
149            g.dispose();
150        }
151        return disabledImgCache;
152    }
153
154    private Image getImage() {
155        if (img != null)
156            return img;
157        temporary = false;
158        loadImage();
159        synchronized (this) {
160            if (img == null) {
161                img = ImageProvider.get("clock").getImage();
162                temporary = true;
163            }
164        }
165        return img;
166    }
167
168    private CompletableFuture<Void> load(Consumer<? super ImageResource> action) {
169        return new ImageProvider(name)
170                .setDirs(MapPaintStyles.getIconSourceDirs(source))
171                .setId("mappaint."+source.getPrefName())
172                .setArchive(source.zipIcons)
173                .setInArchiveDir(source.getZipEntryDirName())
174                .setOptional(true)
175                .getResourceAsync(action);
176    }
177
178    /**
179     * Loads image resource and actual rescaled image.
180     * @return the future of the requested image
181     * @see #loadImageResource
182     */
183    private CompletableFuture<Void> loadImage() {
184        return load(result -> {
185            synchronized (this) {
186                imageResource = result;
187                if (result == null) {
188                    source.logWarning(tr("Failed to locate image ''{0}''", name));
189                    ImageIcon noIcon = MapPaintStyles.getNoIconIcon(source);
190                    img = noIcon == null ? null : noIcon.getImage();
191                } else {
192                    img = result.getImageIcon(new Dimension(width, height)).getImage();
193                    if (img != null && mustRescale(img)) {
194                        // Scale down large images to 16x16 pixels if no size is explicitly specified
195                        img = result.getImageIconBounded(ImageProvider.ImageSizes.MAP.getImageDimension()).getImage();
196                    }
197                }
198                if (temporary) {
199                    disabledImgCache = null;
200                    MapView mapView = MainApplication.getMap().mapView;
201                    mapView.preferenceChanged(null); // otherwise repaint is ignored, because layer hasn't changed
202                    mapView.repaint();
203                }
204                temporary = false;
205            }
206        });
207    }
208
209    /**
210     * Loads image resource only.
211     * @return the future of the requested image resource
212     * @see #loadImage
213     */
214    private CompletableFuture<Void> loadImageResource() {
215        return load(result -> {
216            synchronized (this) {
217                imageResource = result;
218                if (result == null) {
219                    source.logWarning(tr("Failed to locate image ''{0}''", name));
220                }
221            }
222        });
223    }
224
225    /**
226     * Gets the image width
227     * @return The real image width
228     */
229    public int getWidth() {
230        return getImage().getWidth(null);
231    }
232
233    /**
234     * Gets the image height
235     * @return The real image height
236     */
237    public int getHeight() {
238        return getImage().getHeight(null);
239    }
240
241    /**
242     * Gets the alpha value the image should be multiplied with
243     * @return The value in range 0..1
244     */
245    public float getAlphaFloat() {
246        return ColorHelper.int2float(alpha);
247    }
248
249    /**
250     * Determines if image is not completely loaded and {@code getImage()} returns a temporary image.
251     * @return {@code true} if image is not completely loaded and getImage() returns a temporary image
252     */
253    public boolean isTemporary() {
254        return temporary;
255    }
256
257    protected class MapImageBoxProvider implements BoxProvider {
258        @Override
259        public BoxProviderResult get() {
260            return new BoxProviderResult(box(), temporary);
261        }
262
263        private Rectangle box() {
264            int w = getWidth(), h = getHeight();
265            if (mustRescale(getImage())) {
266                w = 16;
267                h = 16;
268            }
269            return new Rectangle(-w/2, -h/2, w, h);
270        }
271
272        private MapImage getParent() {
273            return MapImage.this;
274        }
275
276        @Override
277        public int hashCode() {
278            return MapImage.this.hashCode();
279        }
280
281        @Override
282        public boolean equals(Object obj) {
283            if (!(obj instanceof BoxProvider))
284                return false;
285            if (obj instanceof MapImageBoxProvider) {
286                MapImageBoxProvider other = (MapImageBoxProvider) obj;
287                return MapImage.this.equals(other.getParent());
288            } else if (temporary) {
289                return false;
290            } else {
291                final BoxProvider other = (BoxProvider) obj;
292                BoxProviderResult resultOther = other.get();
293                if (resultOther.isTemporary()) return false;
294                return box().equals(resultOther.getBox());
295            }
296        }
297    }
298
299    /**
300     * Gets a box provider that provides a box that covers the size of this image
301     * @return The box provider
302     */
303    public BoxProvider getBoxProvider() {
304        return new MapImageBoxProvider();
305    }
306
307    private boolean mustRescale(Image image) {
308        return autoRescale && width == -1 && image.getWidth(null) > MAX_SIZE
309             && height == -1 && image.getHeight(null) > MAX_SIZE;
310    }
311
312    @Override
313    public boolean equals(Object obj) {
314        if (this == obj) return true;
315        if (obj == null || getClass() != obj.getClass()) return false;
316        MapImage mapImage = (MapImage) obj;
317        return alpha == mapImage.alpha &&
318                autoRescale == mapImage.autoRescale &&
319                width == mapImage.width &&
320                height == mapImage.height &&
321                Objects.equals(name, mapImage.name) &&
322                Objects.equals(source, mapImage.source);
323    }
324
325    @Override
326    public int hashCode() {
327        return Objects.hash(alpha, name, source, autoRescale, width, height);
328    }
329
330    @Override
331    public String toString() {
332        return name;
333    }
334}