source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/styleelement/MapImage.java@ 16186

Last change on this file since 16186 was 16186, checked in by Don-vip, 5 years ago

fix #18871 - fix performance regression - padded icons requests made unnecessary costly rescale operations

  • Property svn:eol-style set to native
File size: 10.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.styleelement;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Dimension;
7import java.awt.Graphics;
8import java.awt.Image;
9import java.awt.Rectangle;
10import java.awt.image.BufferedImage;
11import java.util.Objects;
12import java.util.concurrent.CompletableFuture;
13import java.util.concurrent.ExecutionException;
14import java.util.function.Consumer;
15
16import javax.swing.ImageIcon;
17
18import org.openstreetmap.josm.gui.MainApplication;
19import org.openstreetmap.josm.gui.MapView;
20import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
21import org.openstreetmap.josm.gui.mappaint.StyleSource;
22import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.BoxProvider;
23import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.BoxProviderResult;
24import org.openstreetmap.josm.gui.util.GuiHelper;
25import org.openstreetmap.josm.tools.ImageProvider;
26import org.openstreetmap.josm.tools.ImageResource;
27import org.openstreetmap.josm.tools.Logging;
28import org.openstreetmap.josm.tools.Utils;
29
30/**
31 * An image that will be displayed on the map.
32 */
33public class MapImage {
34
35 private static final int MAX_SIZE = 48;
36
37 /**
38 * ImageIcon can change while the image is loading.
39 */
40 private Image img;
41 private ImageResource imageResource;
42
43 /**
44 * The alpha (opacity) value of the image. It is multiplied to the image alpha channel.
45 * Range: 0...255
46 */
47 public int alpha = 255;
48 /**
49 * The name of the image that should be displayed. It is given to the {@link ImageProvider}
50 */
51 public String name;
52 /**
53 * The StyleSource that registered the image
54 */
55 public StyleSource source;
56 /**
57 * A flag indicating that the image should automatically be scaled to the right size.
58 */
59 public boolean autoRescale;
60 /**
61 * The width of the image, as set by MapCSS
62 */
63 public int width = -1;
64 /**
65 * The height of the image, as set by MapCSS
66 */
67 public int height = -1;
68 /**
69 * The x offset of the anchor of this image
70 */
71 public int offsetX;
72 /**
73 * The y offset of the anchor of this image
74 */
75 public int offsetY;
76
77 private boolean temporary;
78
79 /**
80 * A cache that holds a disabled (gray) version of this image
81 */
82 private BufferedImage disabledImgCache;
83
84 /**
85 * Creates a new {@link MapImage}
86 * @param name The image name
87 * @param source The style source that requests this image
88 */
89 public MapImage(String name, StyleSource source) {
90 this(name, source, true);
91 }
92
93 /**
94 * Creates a new {@link MapImage}
95 * @param name The image name
96 * @param source The style source that requests this image
97 * @param autoRescale A flag indicating to automatically adjust the width/height of the image
98 */
99 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 = rescale(result.getImageIcon(new Dimension(width, height)).getImage());
193 }
194 if (temporary) {
195 disabledImgCache = null;
196 MapView mapView = MainApplication.getMap().mapView;
197 mapView.preferenceChanged(null); // otherwise repaint is ignored, because layer hasn't changed
198 mapView.repaint();
199 }
200 temporary = false;
201 }
202 });
203 }
204
205 /**
206 * Loads image resource only.
207 * @return the future of the requested image resource
208 * @see #loadImage
209 */
210 private CompletableFuture<Void> loadImageResource() {
211 return load(result -> {
212 synchronized (this) {
213 imageResource = result;
214 if (result == null) {
215 source.logWarning(tr("Failed to locate image ''{0}''", name));
216 }
217 }
218 });
219 }
220
221 /**
222 * Gets the image width
223 * @return The real image width
224 */
225 public int getWidth() {
226 return getImage().getWidth(null);
227 }
228
229 /**
230 * Gets the image height
231 * @return The real image height
232 */
233 public int getHeight() {
234 return getImage().getHeight(null);
235 }
236
237 /**
238 * Gets the alpha value the image should be multiplied with
239 * @return The value in range 0..1
240 */
241 public float getAlphaFloat() {
242 return Utils.colorInt2float(alpha);
243 }
244
245 /**
246 * Determines if image is not completely loaded and {@code getImage()} returns a temporary image.
247 * @return {@code true} if image is not completely loaded and getImage() returns a temporary image
248 */
249 public boolean isTemporary() {
250 return temporary;
251 }
252
253 protected class MapImageBoxProvider implements BoxProvider {
254 @Override
255 public BoxProviderResult get() {
256 return new BoxProviderResult(box(), temporary);
257 }
258
259 private Rectangle box() {
260 int w = getWidth(), h = getHeight();
261 if (mustRescale(getImage())) {
262 w = 16;
263 h = 16;
264 }
265 return new Rectangle(-w/2, -h/2, w, h);
266 }
267
268 private MapImage getParent() {
269 return MapImage.this;
270 }
271
272 @Override
273 public int hashCode() {
274 return MapImage.this.hashCode();
275 }
276
277 @Override
278 public boolean equals(Object obj) {
279 if (!(obj instanceof BoxProvider))
280 return false;
281 if (obj instanceof MapImageBoxProvider) {
282 MapImageBoxProvider other = (MapImageBoxProvider) obj;
283 return MapImage.this.equals(other.getParent());
284 } else if (temporary) {
285 return false;
286 } else {
287 final BoxProvider other = (BoxProvider) obj;
288 BoxProviderResult resultOther = other.get();
289 if (resultOther.isTemporary()) return false;
290 return box().equals(resultOther.getBox());
291 }
292 }
293 }
294
295 /**
296 * Gets a box provider that provides a box that covers the size of this image
297 * @return The box provider
298 */
299 public BoxProvider getBoxProvider() {
300 return new MapImageBoxProvider();
301 }
302
303 /**
304 * Rescale excessively large images.
305 * @param image the unscaled image
306 * @return The scaled down version to 16x16 pixels if the image height and width exceeds 48 pixels and no size has been explicitly specified
307 */
308 private Image rescale(Image image) {
309 if (image == null) return null;
310 // Scale down large (.svg) images to 16x16 pixels if no size is explicitly specified
311 if (mustRescale(image)) {
312 return ImageProvider.createBoundedImage(image, 16);
313 } else {
314 return image;
315 }
316 }
317
318 private boolean mustRescale(Image image) {
319 return autoRescale && width == -1 && image.getWidth(null) > MAX_SIZE
320 && height == -1 && image.getHeight(null) > MAX_SIZE;
321 }
322
323 @Override
324 public boolean equals(Object obj) {
325 if (this == obj) return true;
326 if (obj == null || getClass() != obj.getClass()) return false;
327 MapImage mapImage = (MapImage) obj;
328 return alpha == mapImage.alpha &&
329 autoRescale == mapImage.autoRescale &&
330 width == mapImage.width &&
331 height == mapImage.height &&
332 Objects.equals(name, mapImage.name) &&
333 Objects.equals(source, mapImage.source);
334 }
335
336 @Override
337 public int hashCode() {
338 return Objects.hash(alpha, name, source, autoRescale, width, height);
339 }
340
341 @Override
342 public String toString() {
343 return name;
344 }
345}
Note: See TracBrowser for help on using the repository browser.