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}