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}