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