| 1 | // License: GPL. Copyright 2007 by Immanuel Scholz and others |
|---|
| 2 | package org.openstreetmap.josm.tools; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 5 | |
|---|
| 6 | import java.awt.Component; |
|---|
| 7 | import java.awt.Cursor; |
|---|
| 8 | import java.awt.Dimension; |
|---|
| 9 | import java.awt.Graphics; |
|---|
| 10 | import java.awt.Graphics2D; |
|---|
| 11 | import java.awt.GraphicsConfiguration; |
|---|
| 12 | import java.awt.GraphicsEnvironment; |
|---|
| 13 | import java.awt.Image; |
|---|
| 14 | import java.awt.Point; |
|---|
| 15 | import java.awt.RenderingHints; |
|---|
| 16 | import java.awt.Toolkit; |
|---|
| 17 | import java.awt.Transparency; |
|---|
| 18 | import java.awt.image.BufferedImage; |
|---|
| 19 | import java.io.ByteArrayInputStream; |
|---|
| 20 | import java.io.File; |
|---|
| 21 | import java.io.IOException; |
|---|
| 22 | import java.io.InputStream; |
|---|
| 23 | import java.io.StringReader; |
|---|
| 24 | import java.io.UnsupportedEncodingException; |
|---|
| 25 | import java.net.MalformedURLException; |
|---|
| 26 | import java.net.URI; |
|---|
| 27 | import java.net.URL; |
|---|
| 28 | import java.net.URLDecoder; |
|---|
| 29 | import java.util.ArrayList; |
|---|
| 30 | import java.util.Arrays; |
|---|
| 31 | import java.util.Collection; |
|---|
| 32 | import java.util.HashMap; |
|---|
| 33 | import java.util.Map; |
|---|
| 34 | import java.util.concurrent.Executors; |
|---|
| 35 | import java.util.concurrent.ExecutorService; |
|---|
| 36 | import java.util.regex.Matcher; |
|---|
| 37 | import java.util.regex.Pattern; |
|---|
| 38 | import java.util.zip.ZipEntry; |
|---|
| 39 | import java.util.zip.ZipFile; |
|---|
| 40 | |
|---|
| 41 | import javax.imageio.ImageIO; |
|---|
| 42 | import javax.swing.Icon; |
|---|
| 43 | import javax.swing.ImageIcon; |
|---|
| 44 | |
|---|
| 45 | import org.apache.commons.codec.binary.Base64; |
|---|
| 46 | import org.openstreetmap.josm.Main; |
|---|
| 47 | import org.openstreetmap.josm.data.osm.OsmPrimitiveType; |
|---|
| 48 | import org.openstreetmap.josm.io.MirroredInputStream; |
|---|
| 49 | import org.openstreetmap.josm.plugins.PluginHandler; |
|---|
| 50 | import org.xml.sax.Attributes; |
|---|
| 51 | import org.xml.sax.EntityResolver; |
|---|
| 52 | import org.xml.sax.InputSource; |
|---|
| 53 | import org.xml.sax.SAXException; |
|---|
| 54 | import org.xml.sax.XMLReader; |
|---|
| 55 | import org.xml.sax.helpers.DefaultHandler; |
|---|
| 56 | import org.xml.sax.helpers.XMLReaderFactory; |
|---|
| 57 | |
|---|
| 58 | import com.kitfox.svg.SVGDiagram; |
|---|
| 59 | import com.kitfox.svg.SVGException; |
|---|
| 60 | import com.kitfox.svg.SVGUniverse; |
|---|
| 61 | |
|---|
| 62 | /** |
|---|
| 63 | * Helper class to support the application with images. |
|---|
| 64 | * |
|---|
| 65 | * How to use: |
|---|
| 66 | * |
|---|
| 67 | * <code>ImageIcon icon = new ImageProvider(name).setMaxWidth(24).setMaxHeight(24).get();</code> |
|---|
| 68 | * (there are more options, see below) |
|---|
| 69 | * |
|---|
| 70 | * short form: |
|---|
| 71 | * <code>ImageIcon icon = ImageProvider.get(name);</code> |
|---|
| 72 | * |
|---|
| 73 | * @author imi |
|---|
| 74 | */ |
|---|
| 75 | public class ImageProvider { |
|---|
| 76 | |
|---|
| 77 | /** |
|---|
| 78 | * Position of an overlay icon |
|---|
| 79 | * @author imi |
|---|
| 80 | */ |
|---|
| 81 | public static enum OverlayPosition { |
|---|
| 82 | NORTHWEST, NORTHEAST, SOUTHWEST, SOUTHEAST |
|---|
| 83 | } |
|---|
| 84 | |
|---|
| 85 | public static enum ImageType { |
|---|
| 86 | SVG, // scalable vector graphics |
|---|
| 87 | OTHER // everything else, e.g. png, gif (must be supported by Java) |
|---|
| 88 | } |
|---|
| 89 | |
|---|
| 90 | protected Collection<String> dirs; |
|---|
| 91 | protected String id; |
|---|
| 92 | protected String subdir; |
|---|
| 93 | protected String name; |
|---|
| 94 | protected File archive; |
|---|
| 95 | protected int width = -1; |
|---|
| 96 | protected int height = -1; |
|---|
| 97 | protected int maxWidth = -1; |
|---|
| 98 | protected int maxHeight = -1; |
|---|
| 99 | protected boolean optional; |
|---|
| 100 | protected boolean suppressWarnings; |
|---|
| 101 | protected Collection<ClassLoader> additionalClassLoaders; |
|---|
| 102 | |
|---|
| 103 | private static SVGUniverse svgUniverse; |
|---|
| 104 | |
|---|
| 105 | /** |
|---|
| 106 | * The icon cache |
|---|
| 107 | */ |
|---|
| 108 | private static Map<String, ImageResource> cache = new HashMap<String, ImageResource>(); |
|---|
| 109 | |
|---|
| 110 | private final static ExecutorService imageFetcher = Executors.newSingleThreadExecutor(); |
|---|
| 111 | |
|---|
| 112 | public interface ImageCallback { |
|---|
| 113 | void finished(ImageIcon result); |
|---|
| 114 | } |
|---|
| 115 | |
|---|
| 116 | /** |
|---|
| 117 | * @param subdir Subdirectory the image lies in. |
|---|
| 118 | * @param name The name of the image. If it does not end with '.png' or '.svg', |
|---|
| 119 | * both extensions are tried. |
|---|
| 120 | */ |
|---|
| 121 | public ImageProvider(String subdir, String name) { |
|---|
| 122 | this.subdir = subdir; |
|---|
| 123 | this.name = name; |
|---|
| 124 | } |
|---|
| 125 | |
|---|
| 126 | public ImageProvider(String name) { |
|---|
| 127 | this.name = name; |
|---|
| 128 | } |
|---|
| 129 | |
|---|
| 130 | /** |
|---|
| 131 | * Directories to look for the image. |
|---|
| 132 | */ |
|---|
| 133 | public ImageProvider setDirs(Collection<String> dirs) { |
|---|
| 134 | this.dirs = dirs; |
|---|
| 135 | return this; |
|---|
| 136 | } |
|---|
| 137 | |
|---|
| 138 | /** |
|---|
| 139 | * An id used for caching. Id is not used for cache if name starts with http://. (URL is unique anyway.) |
|---|
| 140 | */ |
|---|
| 141 | public ImageProvider setId(String id) { |
|---|
| 142 | this.id = id; |
|---|
| 143 | return this; |
|---|
| 144 | } |
|---|
| 145 | |
|---|
| 146 | /** |
|---|
| 147 | * A zip file where the image is located. |
|---|
| 148 | */ |
|---|
| 149 | public ImageProvider setArchive(File archive) { |
|---|
| 150 | this.archive = archive; |
|---|
| 151 | return this; |
|---|
| 152 | } |
|---|
| 153 | |
|---|
| 154 | /** |
|---|
| 155 | * The dimensions of the image. |
|---|
| 156 | * |
|---|
| 157 | * If not specified, the original size of the image is used. |
|---|
| 158 | * The width part of the dimension can be -1. Then it will only set the height but |
|---|
| 159 | * keep the aspect ratio. (And the other way around.) |
|---|
| 160 | */ |
|---|
| 161 | public ImageProvider setSize(Dimension size) { |
|---|
| 162 | this.width = size.width; |
|---|
| 163 | this.height = size.height; |
|---|
| 164 | return this; |
|---|
| 165 | } |
|---|
| 166 | |
|---|
| 167 | /** |
|---|
| 168 | * see setSize |
|---|
| 169 | */ |
|---|
| 170 | public ImageProvider setWidth(int width) { |
|---|
| 171 | this.width = width; |
|---|
| 172 | return this; |
|---|
| 173 | } |
|---|
| 174 | |
|---|
| 175 | /** |
|---|
| 176 | * see setSize |
|---|
| 177 | */ |
|---|
| 178 | public ImageProvider setHeight(int height) { |
|---|
| 179 | this.height = height; |
|---|
| 180 | return this; |
|---|
| 181 | } |
|---|
| 182 | |
|---|
| 183 | /** |
|---|
| 184 | * The maximum size of the image. |
|---|
| 185 | * |
|---|
| 186 | * It will shrink the image if necessary, but keep the aspect ratio. |
|---|
| 187 | * The given width or height can be -1 which means this direction is not bounded. |
|---|
| 188 | * |
|---|
| 189 | * 'size' and 'maxSize' are not compatible, you should set only one of them. |
|---|
| 190 | */ |
|---|
| 191 | public ImageProvider setMaxSize(Dimension maxSize) { |
|---|
| 192 | this.maxWidth = maxSize.width; |
|---|
| 193 | this.maxHeight = maxSize.height; |
|---|
| 194 | return this; |
|---|
| 195 | } |
|---|
| 196 | |
|---|
| 197 | /** |
|---|
| 198 | * see setMaxSize |
|---|
| 199 | */ |
|---|
| 200 | public ImageProvider setMaxWidth(int maxWidth) { |
|---|
| 201 | this.maxWidth = maxWidth; |
|---|
| 202 | return this; |
|---|
| 203 | } |
|---|
| 204 | |
|---|
| 205 | /** |
|---|
| 206 | * see setMaxSize |
|---|
| 207 | */ |
|---|
| 208 | public ImageProvider setMaxHeight(int maxHeight) { |
|---|
| 209 | this.maxHeight = maxHeight; |
|---|
| 210 | return this; |
|---|
| 211 | } |
|---|
| 212 | |
|---|
| 213 | /** |
|---|
| 214 | * The image URL comes from user data and the image may be missing. |
|---|
| 215 | * |
|---|
| 216 | * Set true, if JOSM should *not* throw a RuntimeException in case the image cannot be located. |
|---|
| 217 | */ |
|---|
| 218 | public ImageProvider setOptional(boolean optional) { |
|---|
| 219 | this.optional = optional; |
|---|
| 220 | return this; |
|---|
| 221 | } |
|---|
| 222 | |
|---|
| 223 | /** |
|---|
| 224 | * Suppresses warning on the command line in case the image cannot be found. |
|---|
| 225 | * |
|---|
| 226 | * In combination with setOptional(true); |
|---|
| 227 | */ |
|---|
| 228 | public ImageProvider setSuppressWarnings(boolean suppressWarnings) { |
|---|
| 229 | this.suppressWarnings = suppressWarnings; |
|---|
| 230 | return this; |
|---|
| 231 | } |
|---|
| 232 | |
|---|
| 233 | /** |
|---|
| 234 | * Add a collection of additional class loaders to search image for. |
|---|
| 235 | */ |
|---|
| 236 | public ImageProvider setAdditionalClassLoaders(Collection<ClassLoader> additionalClassLoaders) { |
|---|
| 237 | this.additionalClassLoaders = additionalClassLoaders; |
|---|
| 238 | return this; |
|---|
| 239 | } |
|---|
| 240 | |
|---|
| 241 | /** |
|---|
| 242 | * Execute the image request. |
|---|
| 243 | */ |
|---|
| 244 | public ImageIcon get() { |
|---|
| 245 | ImageResource ir = getIfAvailableImpl(additionalClassLoaders); |
|---|
| 246 | if (ir == null) { |
|---|
| 247 | if (!optional) { |
|---|
| 248 | String ext = name.indexOf('.') != -1 ? "" : ".???"; |
|---|
| 249 | throw new RuntimeException(tr("Fatal: failed to locate image ''{0}''. This is a serious configuration problem. JOSM will stop working.", name + ext)); |
|---|
| 250 | } else { |
|---|
| 251 | if (!suppressWarnings) { |
|---|
| 252 | System.err.println(tr("Failed to locate image ''{0}''", name)); |
|---|
| 253 | } |
|---|
| 254 | return null; |
|---|
| 255 | } |
|---|
| 256 | } |
|---|
| 257 | if (maxWidth != -1 || maxHeight != -1) |
|---|
| 258 | return ir.getImageIconBounded(new Dimension(maxWidth, maxHeight)); |
|---|
| 259 | else |
|---|
| 260 | return ir.getImageIcon(new Dimension(width, height)); |
|---|
| 261 | } |
|---|
| 262 | |
|---|
| 263 | /** |
|---|
| 264 | * Load the image in a background thread. |
|---|
| 265 | * |
|---|
| 266 | * This method returns immediately and runs the image request |
|---|
| 267 | * asynchronously. |
|---|
| 268 | * |
|---|
| 269 | * @param callback is called, when the image is ready. This can happen |
|---|
| 270 | * before the call to getInBackground returns or it may be invoked some |
|---|
| 271 | * time (seconds) later. |
|---|
| 272 | * If no image is available, a null value is returned to callback (just |
|---|
| 273 | * like ImageProvider.get). |
|---|
| 274 | */ |
|---|
| 275 | public void getInBackground(final ImageCallback callback) { |
|---|
| 276 | if (name.startsWith("http://") || name.startsWith("wiki://")) { |
|---|
| 277 | Runnable fetch = new Runnable() { |
|---|
| 278 | @Override |
|---|
| 279 | public void run() { |
|---|
| 280 | ImageIcon result = get(); |
|---|
| 281 | callback.finished(result); |
|---|
| 282 | } |
|---|
| 283 | }; |
|---|
| 284 | imageFetcher.submit(fetch); |
|---|
| 285 | } else { |
|---|
| 286 | ImageIcon result = get(); |
|---|
| 287 | callback.finished(result); |
|---|
| 288 | } |
|---|
| 289 | } |
|---|
| 290 | |
|---|
| 291 | /** |
|---|
| 292 | * Return an image from the specified location. Throws a RuntimeException if |
|---|
| 293 | * the image cannot be located. |
|---|
| 294 | * |
|---|
| 295 | * @param subdir The position of the directory, e.g. 'layer' |
|---|
| 296 | * @param name The icons name (with or without '.png' or '.svg' extension) |
|---|
| 297 | * @return The requested Image. |
|---|
| 298 | */ |
|---|
| 299 | public static ImageIcon get(String subdir, String name) { |
|---|
| 300 | return new ImageProvider(subdir, name).get(); |
|---|
| 301 | } |
|---|
| 302 | |
|---|
| 303 | public static ImageIcon get(String name) { |
|---|
| 304 | return new ImageProvider(name).get(); |
|---|
| 305 | } |
|---|
| 306 | |
|---|
| 307 | public static ImageIcon getIfAvailable(String name) { |
|---|
| 308 | return new ImageProvider(name).setOptional(true).get(); |
|---|
| 309 | } |
|---|
| 310 | |
|---|
| 311 | public static ImageIcon getIfAvailable(String subdir, String name) { |
|---|
| 312 | return new ImageProvider(subdir, name).setOptional(true).get(); |
|---|
| 313 | } |
|---|
| 314 | |
|---|
| 315 | /** |
|---|
| 316 | * {@code data:[<mediatype>][;base64],<data>} |
|---|
| 317 | * @see RFC2397 |
|---|
| 318 | */ |
|---|
| 319 | private static final Pattern dataUrlPattern = Pattern.compile( |
|---|
| 320 | "^data:([a-zA-Z]+/[a-zA-Z+]+)?(;base64)?,(.+)$"); |
|---|
| 321 | |
|---|
| 322 | private ImageResource getIfAvailableImpl(Collection<ClassLoader> additionalClassLoaders) { |
|---|
| 323 | if (name == null) |
|---|
| 324 | return null; |
|---|
| 325 | |
|---|
| 326 | try { |
|---|
| 327 | if (name.startsWith("data:")) { |
|---|
| 328 | Matcher m = dataUrlPattern.matcher(name); |
|---|
| 329 | if (m.matches()) { |
|---|
| 330 | String mediatype = m.group(1); |
|---|
| 331 | String base64 = m.group(2); |
|---|
| 332 | String data = m.group(3); |
|---|
| 333 | byte[] bytes = ";base64".equals(base64) |
|---|
| 334 | ? Base64.decodeBase64(data) |
|---|
| 335 | : URLDecoder.decode(data, "utf-8").getBytes(); |
|---|
| 336 | if (mediatype != null && mediatype.contains("image/svg+xml")) { |
|---|
| 337 | URI uri = getSvgUniverse().loadSVG(new StringReader(new String(bytes)), name); |
|---|
| 338 | return new ImageResource(getSvgUniverse().getDiagram(uri)); |
|---|
| 339 | } else { |
|---|
| 340 | try { |
|---|
| 341 | return new ImageResource(ImageIO.read(new ByteArrayInputStream(bytes))); |
|---|
| 342 | } catch (IOException e) {} |
|---|
| 343 | } |
|---|
| 344 | } |
|---|
| 345 | } |
|---|
| 346 | } catch (UnsupportedEncodingException ex) { |
|---|
| 347 | throw new RuntimeException(ex.getMessage(), ex); |
|---|
| 348 | } catch (IOException ex) { |
|---|
| 349 | throw new RuntimeException(ex.getMessage(), ex); |
|---|
| 350 | } |
|---|
| 351 | |
|---|
| 352 | ImageType type = name.toLowerCase().endsWith(".svg") ? ImageType.SVG : ImageType.OTHER; |
|---|
| 353 | |
|---|
| 354 | if (name.startsWith("http://")) { |
|---|
| 355 | String url = name; |
|---|
| 356 | ImageResource ir = cache.get(url); |
|---|
| 357 | if (ir != null) return ir; |
|---|
| 358 | ir = getIfAvailableHttp(url, type); |
|---|
| 359 | if (ir != null) { |
|---|
| 360 | cache.put(url, ir); |
|---|
| 361 | } |
|---|
| 362 | return ir; |
|---|
| 363 | } else if (name.startsWith("wiki://")) { |
|---|
| 364 | ImageResource ir = cache.get(name); |
|---|
| 365 | if (ir != null) return ir; |
|---|
| 366 | ir = getIfAvailableWiki(name, type); |
|---|
| 367 | if (ir != null) { |
|---|
| 368 | cache.put(name, ir); |
|---|
| 369 | } |
|---|
| 370 | return ir; |
|---|
| 371 | } |
|---|
| 372 | |
|---|
| 373 | if (subdir == null) { |
|---|
| 374 | subdir = ""; |
|---|
| 375 | } else if (!subdir.equals("")) { |
|---|
| 376 | subdir += "/"; |
|---|
| 377 | } |
|---|
| 378 | String[] extensions; |
|---|
| 379 | if (name.indexOf('.') != -1) { |
|---|
| 380 | extensions = new String[] { "" }; |
|---|
| 381 | } else { |
|---|
| 382 | extensions = new String[] { ".png", ".svg"}; |
|---|
| 383 | } |
|---|
| 384 | final int ARCHIVE = 0, LOCAL = 1; |
|---|
| 385 | for (int place : new Integer[] { ARCHIVE, LOCAL }) { |
|---|
| 386 | for (String ext : extensions) { |
|---|
| 387 | |
|---|
| 388 | if (".svg".equals(ext)) { |
|---|
| 389 | type = ImageType.SVG; |
|---|
| 390 | } else if (".png".equals(ext)) { |
|---|
| 391 | type = ImageType.OTHER; |
|---|
| 392 | } |
|---|
| 393 | |
|---|
| 394 | String full_name = subdir + name + ext; |
|---|
| 395 | String cache_name = full_name; |
|---|
| 396 | /* cache separately */ |
|---|
| 397 | if (dirs != null && dirs.size() > 0) { |
|---|
| 398 | cache_name = "id:" + id + ":" + full_name; |
|---|
| 399 | if(archive != null) { |
|---|
| 400 | cache_name += ":" + archive.getName(); |
|---|
| 401 | } |
|---|
| 402 | } |
|---|
| 403 | |
|---|
| 404 | ImageResource ir = cache.get(cache_name); |
|---|
| 405 | if (ir != null) return ir; |
|---|
| 406 | |
|---|
| 407 | switch (place) { |
|---|
| 408 | case ARCHIVE: |
|---|
| 409 | if (archive != null) { |
|---|
| 410 | ir = getIfAvailableZip(full_name, archive, type); |
|---|
| 411 | if (ir != null) { |
|---|
| 412 | cache.put(cache_name, ir); |
|---|
| 413 | return ir; |
|---|
| 414 | } |
|---|
| 415 | } |
|---|
| 416 | break; |
|---|
| 417 | case LOCAL: |
|---|
| 418 | // getImageUrl() does a ton of "stat()" calls and gets expensive |
|---|
| 419 | // and redundant when you have a whole ton of objects. So, |
|---|
| 420 | // index the cache by the name of the icon we're looking for |
|---|
| 421 | // and don't bother to create a URL unless we're actually |
|---|
| 422 | // creating the image. |
|---|
| 423 | URL path = getImageUrl(full_name, dirs, additionalClassLoaders); |
|---|
| 424 | if (path == null) { |
|---|
| 425 | continue; |
|---|
| 426 | } |
|---|
| 427 | ir = getIfAvailableLocalURL(path, type); |
|---|
| 428 | if (ir != null) { |
|---|
| 429 | cache.put(cache_name, ir); |
|---|
| 430 | return ir; |
|---|
| 431 | } |
|---|
| 432 | break; |
|---|
| 433 | } |
|---|
| 434 | } |
|---|
| 435 | } |
|---|
| 436 | return null; |
|---|
| 437 | } |
|---|
| 438 | |
|---|
| 439 | private static ImageResource getIfAvailableHttp(String url, ImageType type) { |
|---|
| 440 | try { |
|---|
| 441 | MirroredInputStream is = new MirroredInputStream(url, |
|---|
| 442 | new File(Main.pref.getCacheDirectory(), "images").getPath()); |
|---|
| 443 | switch (type) { |
|---|
| 444 | case SVG: |
|---|
| 445 | URI uri = getSvgUniverse().loadSVG(is, is.getFile().toURI().toURL().toString()); |
|---|
| 446 | SVGDiagram svg = getSvgUniverse().getDiagram(uri); |
|---|
| 447 | return svg == null ? null : new ImageResource(svg); |
|---|
| 448 | case OTHER: |
|---|
| 449 | BufferedImage img = null; |
|---|
| 450 | try { |
|---|
| 451 | img = ImageIO.read(is.getFile().toURI().toURL()); |
|---|
| 452 | } catch (IOException e) {} |
|---|
| 453 | return img == null ? null : new ImageResource(img); |
|---|
| 454 | default: |
|---|
| 455 | throw new AssertionError(); |
|---|
| 456 | } |
|---|
| 457 | } catch (IOException e) { |
|---|
| 458 | return null; |
|---|
| 459 | } |
|---|
| 460 | } |
|---|
| 461 | |
|---|
| 462 | private static ImageResource getIfAvailableWiki(String name, ImageType type) { |
|---|
| 463 | final Collection<String> defaultBaseUrls = Arrays.asList( |
|---|
| 464 | "http://wiki.openstreetmap.org/w/images/", |
|---|
| 465 | "http://upload.wikimedia.org/wikipedia/commons/", |
|---|
| 466 | "http://wiki.openstreetmap.org/wiki/File:" |
|---|
| 467 | ); |
|---|
| 468 | final Collection<String> baseUrls = Main.pref.getCollection("image-provider.wiki.urls", defaultBaseUrls); |
|---|
| 469 | |
|---|
| 470 | final String fn = name.substring(name.lastIndexOf('/') + 1); |
|---|
| 471 | |
|---|
| 472 | ImageResource result = null; |
|---|
| 473 | for (String b : baseUrls) { |
|---|
| 474 | String url; |
|---|
| 475 | if (b.endsWith(":")) { |
|---|
| 476 | url = getImgUrlFromWikiInfoPage(b, fn); |
|---|
| 477 | if (url == null) { |
|---|
| 478 | continue; |
|---|
| 479 | } |
|---|
| 480 | } else { |
|---|
| 481 | final String fn_md5 = Utils.md5Hex(fn); |
|---|
| 482 | url = b + fn_md5.substring(0,1) + "/" + fn_md5.substring(0,2) + "/" + fn; |
|---|
| 483 | } |
|---|
| 484 | result = getIfAvailableHttp(url, type); |
|---|
| 485 | if (result != null) { |
|---|
| 486 | break; |
|---|
| 487 | } |
|---|
| 488 | } |
|---|
| 489 | return result; |
|---|
| 490 | } |
|---|
| 491 | |
|---|
| 492 | private static ImageResource getIfAvailableZip(String full_name, File archive, ImageType type) { |
|---|
| 493 | ZipFile zipFile = null; |
|---|
| 494 | try |
|---|
| 495 | { |
|---|
| 496 | zipFile = new ZipFile(archive); |
|---|
| 497 | ZipEntry entry = zipFile.getEntry(full_name); |
|---|
| 498 | if(entry != null) |
|---|
| 499 | { |
|---|
| 500 | int size = (int)entry.getSize(); |
|---|
| 501 | int offs = 0; |
|---|
| 502 | byte[] buf = new byte[size]; |
|---|
| 503 | InputStream is = null; |
|---|
| 504 | try { |
|---|
| 505 | is = zipFile.getInputStream(entry); |
|---|
| 506 | switch (type) { |
|---|
| 507 | case SVG: |
|---|
| 508 | URI uri = getSvgUniverse().loadSVG(is, full_name); |
|---|
| 509 | SVGDiagram svg = getSvgUniverse().getDiagram(uri); |
|---|
| 510 | return svg == null ? null : new ImageResource(svg); |
|---|
| 511 | case OTHER: |
|---|
| 512 | while(size > 0) |
|---|
| 513 | { |
|---|
| 514 | int l = is.read(buf, offs, size); |
|---|
| 515 | offs += l; |
|---|
| 516 | size -= l; |
|---|
| 517 | } |
|---|
| 518 | BufferedImage img = null; |
|---|
| 519 | try { |
|---|
| 520 | img = ImageIO.read(new ByteArrayInputStream(buf)); |
|---|
| 521 | } catch (IOException e) {} |
|---|
| 522 | return img == null ? null : new ImageResource(img); |
|---|
| 523 | default: |
|---|
| 524 | throw new AssertionError(); |
|---|
| 525 | } |
|---|
| 526 | } finally { |
|---|
| 527 | if (is != null) { |
|---|
| 528 | is.close(); |
|---|
| 529 | } |
|---|
| 530 | } |
|---|
| 531 | } |
|---|
| 532 | } catch (Exception e) { |
|---|
| 533 | System.err.println(tr("Warning: failed to handle zip file ''{0}''. Exception was: {1}", archive.getName(), e.toString())); |
|---|
| 534 | } finally { |
|---|
| 535 | if (zipFile != null) { |
|---|
| 536 | try { |
|---|
| 537 | zipFile.close(); |
|---|
| 538 | } catch (IOException ex) { |
|---|
| 539 | } |
|---|
| 540 | } |
|---|
| 541 | } |
|---|
| 542 | return null; |
|---|
| 543 | } |
|---|
| 544 | |
|---|
| 545 | private static ImageResource getIfAvailableLocalURL(URL path, ImageType type) { |
|---|
| 546 | switch (type) { |
|---|
| 547 | case SVG: |
|---|
| 548 | URI uri = getSvgUniverse().loadSVG(path); |
|---|
| 549 | SVGDiagram svg = getSvgUniverse().getDiagram(uri); |
|---|
| 550 | return svg == null ? null : new ImageResource(svg); |
|---|
| 551 | case OTHER: |
|---|
| 552 | BufferedImage img = null; |
|---|
| 553 | try { |
|---|
| 554 | img = ImageIO.read(path); |
|---|
| 555 | } catch (IOException e) {} |
|---|
| 556 | return img == null ? null : new ImageResource(img); |
|---|
| 557 | default: |
|---|
| 558 | throw new AssertionError(); |
|---|
| 559 | } |
|---|
| 560 | } |
|---|
| 561 | |
|---|
| 562 | private static URL getImageUrl(String path, String name, Collection<ClassLoader> additionalClassLoaders) { |
|---|
| 563 | if (path != null && path.startsWith("resource://")) { |
|---|
| 564 | String p = path.substring("resource://".length()); |
|---|
| 565 | Collection<ClassLoader> classLoaders = new ArrayList<ClassLoader>(PluginHandler.getResourceClassLoaders()); |
|---|
| 566 | if (additionalClassLoaders != null) { |
|---|
| 567 | classLoaders.addAll(additionalClassLoaders); |
|---|
| 568 | } |
|---|
| 569 | for (ClassLoader source : classLoaders) { |
|---|
| 570 | URL res; |
|---|
| 571 | if ((res = source.getResource(p + name)) != null) |
|---|
| 572 | return res; |
|---|
| 573 | } |
|---|
| 574 | } else { |
|---|
| 575 | try { |
|---|
| 576 | File f = new File(path, name); |
|---|
| 577 | if (f.exists()) |
|---|
| 578 | return f.toURI().toURL(); |
|---|
| 579 | } catch (MalformedURLException e) { |
|---|
| 580 | } |
|---|
| 581 | } |
|---|
| 582 | return null; |
|---|
| 583 | } |
|---|
| 584 | |
|---|
| 585 | private static URL getImageUrl(String imageName, Collection<String> dirs, Collection<ClassLoader> additionalClassLoaders) { |
|---|
| 586 | URL u = null; |
|---|
| 587 | |
|---|
| 588 | // Try passed directories first |
|---|
| 589 | if (dirs != null) { |
|---|
| 590 | for (String name : dirs) { |
|---|
| 591 | try { |
|---|
| 592 | u = getImageUrl(name, imageName, additionalClassLoaders); |
|---|
| 593 | if (u != null) |
|---|
| 594 | return u; |
|---|
| 595 | } catch (SecurityException e) { |
|---|
| 596 | System.out.println(tr( |
|---|
| 597 | "Warning: failed to access directory ''{0}'' for security reasons. Exception was: {1}", |
|---|
| 598 | name, e.toString())); |
|---|
| 599 | } |
|---|
| 600 | |
|---|
| 601 | } |
|---|
| 602 | } |
|---|
| 603 | // Try user-preference directory |
|---|
| 604 | String dir = Main.pref.getPreferencesDir() + "images"; |
|---|
| 605 | try { |
|---|
| 606 | u = getImageUrl(dir, imageName, additionalClassLoaders); |
|---|
| 607 | if (u != null) |
|---|
| 608 | return u; |
|---|
| 609 | } catch (SecurityException e) { |
|---|
| 610 | System.out.println(tr( |
|---|
| 611 | "Warning: failed to access directory ''{0}'' for security reasons. Exception was: {1}", dir, e |
|---|
| 612 | .toString())); |
|---|
| 613 | } |
|---|
| 614 | |
|---|
| 615 | // Absolute path? |
|---|
| 616 | u = getImageUrl(null, imageName, additionalClassLoaders); |
|---|
| 617 | if (u != null) |
|---|
| 618 | return u; |
|---|
| 619 | |
|---|
| 620 | // Try plugins and josm classloader |
|---|
| 621 | u = getImageUrl("resource://images/", imageName, additionalClassLoaders); |
|---|
| 622 | if (u != null) |
|---|
| 623 | return u; |
|---|
| 624 | |
|---|
| 625 | // Try all other resource directories |
|---|
| 626 | for (String location : Main.pref.getAllPossiblePreferenceDirs()) { |
|---|
| 627 | u = getImageUrl(location + "images", imageName, additionalClassLoaders); |
|---|
| 628 | if (u != null) |
|---|
| 629 | return u; |
|---|
| 630 | u = getImageUrl(location, imageName, additionalClassLoaders); |
|---|
| 631 | if (u != null) |
|---|
| 632 | return u; |
|---|
| 633 | } |
|---|
| 634 | |
|---|
| 635 | return null; |
|---|
| 636 | } |
|---|
| 637 | |
|---|
| 638 | /** |
|---|
| 639 | * Reads the wiki page on a certain file in html format in order to find the real image URL. |
|---|
| 640 | */ |
|---|
| 641 | private static String getImgUrlFromWikiInfoPage(final String base, final String fn) { |
|---|
| 642 | |
|---|
| 643 | /** Quit parsing, when a certain condition is met */ |
|---|
| 644 | class SAXReturnException extends SAXException { |
|---|
| 645 | private String result; |
|---|
| 646 | |
|---|
| 647 | public SAXReturnException(String result) { |
|---|
| 648 | this.result = result; |
|---|
| 649 | } |
|---|
| 650 | |
|---|
| 651 | public String getResult() { |
|---|
| 652 | return result; |
|---|
| 653 | } |
|---|
| 654 | } |
|---|
| 655 | |
|---|
| 656 | try { |
|---|
| 657 | final XMLReader parser = XMLReaderFactory.createXMLReader(); |
|---|
| 658 | parser.setContentHandler(new DefaultHandler() { |
|---|
| 659 | @Override |
|---|
| 660 | public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { |
|---|
| 661 | System.out.println(); |
|---|
| 662 | if (localName.equalsIgnoreCase("img")) { |
|---|
| 663 | String val = atts.getValue("src"); |
|---|
| 664 | if (val.endsWith(fn)) |
|---|
| 665 | throw new SAXReturnException(val); // parsing done, quit early |
|---|
| 666 | } |
|---|
| 667 | } |
|---|
| 668 | }); |
|---|
| 669 | |
|---|
| 670 | parser.setEntityResolver(new EntityResolver() { |
|---|
| 671 | public InputSource resolveEntity (String publicId, String systemId) { |
|---|
| 672 | return new InputSource(new ByteArrayInputStream(new byte[0])); |
|---|
| 673 | } |
|---|
| 674 | }); |
|---|
| 675 | |
|---|
| 676 | parser.parse(new InputSource(new MirroredInputStream( |
|---|
| 677 | base + fn, |
|---|
| 678 | new File(Main.pref.getPreferencesDir(), "images").toString() |
|---|
| 679 | ))); |
|---|
| 680 | } catch (SAXReturnException r) { |
|---|
| 681 | return r.getResult(); |
|---|
| 682 | } catch (Exception e) { |
|---|
| 683 | System.out.println("INFO: parsing " + base + fn + " failed:\n" + e); |
|---|
| 684 | return null; |
|---|
| 685 | } |
|---|
| 686 | System.out.println("INFO: parsing " + base + fn + " failed: Unexpected content."); |
|---|
| 687 | return null; |
|---|
| 688 | } |
|---|
| 689 | |
|---|
| 690 | public static Cursor getCursor(String name, String overlay) { |
|---|
| 691 | ImageIcon img = get("cursor", name); |
|---|
| 692 | if (overlay != null) { |
|---|
| 693 | img = overlay(img, "cursor/modifier/" + overlay, OverlayPosition.SOUTHEAST); |
|---|
| 694 | } |
|---|
| 695 | Cursor c = Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(), |
|---|
| 696 | name.equals("crosshair") ? new Point(10, 10) : new Point(3, 2), "Cursor"); |
|---|
| 697 | return c; |
|---|
| 698 | } |
|---|
| 699 | |
|---|
| 700 | /** |
|---|
| 701 | * @return an icon that represent the overlay of the two given icons. The second icon is layed |
|---|
| 702 | * on the first relative to the given position. |
|---|
| 703 | */ |
|---|
| 704 | public static ImageIcon overlay(Icon ground, String overlayImage, OverlayPosition pos) { |
|---|
| 705 | return overlay(ground, ImageProvider.get(overlayImage), pos); |
|---|
| 706 | } |
|---|
| 707 | |
|---|
| 708 | public static ImageIcon overlay(Icon ground, Icon overlay, OverlayPosition pos) { |
|---|
| 709 | GraphicsConfiguration conf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice() |
|---|
| 710 | .getDefaultConfiguration(); |
|---|
| 711 | int w = ground.getIconWidth(); |
|---|
| 712 | int h = ground.getIconHeight(); |
|---|
| 713 | int wo = overlay.getIconWidth(); |
|---|
| 714 | int ho = overlay.getIconHeight(); |
|---|
| 715 | BufferedImage img = conf.createCompatibleImage(w, h, Transparency.TRANSLUCENT); |
|---|
| 716 | Graphics g = img.createGraphics(); |
|---|
| 717 | ground.paintIcon(null, g, 0, 0); |
|---|
| 718 | int x = 0, y = 0; |
|---|
| 719 | switch (pos) { |
|---|
| 720 | case NORTHWEST: |
|---|
| 721 | x = 0; |
|---|
| 722 | y = 0; |
|---|
| 723 | break; |
|---|
| 724 | case NORTHEAST: |
|---|
| 725 | x = w - wo; |
|---|
| 726 | y = 0; |
|---|
| 727 | break; |
|---|
| 728 | case SOUTHWEST: |
|---|
| 729 | x = 0; |
|---|
| 730 | y = h - ho; |
|---|
| 731 | break; |
|---|
| 732 | case SOUTHEAST: |
|---|
| 733 | x = w - wo; |
|---|
| 734 | y = h - ho; |
|---|
| 735 | break; |
|---|
| 736 | } |
|---|
| 737 | overlay.paintIcon(null, g, x, y); |
|---|
| 738 | return new ImageIcon(img); |
|---|
| 739 | } |
|---|
| 740 | |
|---|
| 741 | /* |
|---|
| 742 | * from: http://www.jidesoft.com/blog/2008/02/29/rotate-an-icon-in-java/ License: |
|---|
| 743 | * "feel free to use" |
|---|
| 744 | */ |
|---|
| 745 | final static double DEGREE_90 = 90.0 * Math.PI / 180.0; |
|---|
| 746 | |
|---|
| 747 | /** |
|---|
| 748 | * Creates a rotated version of the input image. |
|---|
| 749 | * |
|---|
| 750 | * @param c The component to get properties useful for painting, e.g. the foreground or |
|---|
| 751 | * background color. |
|---|
| 752 | * @param icon the image to be rotated. |
|---|
| 753 | * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we |
|---|
| 754 | * will mod it with 360 before using it. |
|---|
| 755 | * |
|---|
| 756 | * @return the image after rotating. |
|---|
| 757 | */ |
|---|
| 758 | public static Image createRotatedImage(Component c, Image img, double rotatedAngle) { |
|---|
| 759 | // convert rotatedAngle to a value from 0 to 360 |
|---|
| 760 | double originalAngle = rotatedAngle % 360; |
|---|
| 761 | if (rotatedAngle != 0 && originalAngle == 0) { |
|---|
| 762 | originalAngle = 360.0; |
|---|
| 763 | } |
|---|
| 764 | |
|---|
| 765 | // convert originalAngle to a value from 0 to 90 |
|---|
| 766 | double angle = originalAngle % 90; |
|---|
| 767 | if (originalAngle != 0.0 && angle == 0.0) { |
|---|
| 768 | angle = 90.0; |
|---|
| 769 | } |
|---|
| 770 | |
|---|
| 771 | double radian = Math.toRadians(angle); |
|---|
| 772 | |
|---|
| 773 | new ImageIcon(img); // load completely |
|---|
| 774 | int iw = img.getWidth(null); |
|---|
| 775 | int ih = img.getHeight(null); |
|---|
| 776 | int w; |
|---|
| 777 | int h; |
|---|
| 778 | |
|---|
| 779 | if ((originalAngle >= 0 && originalAngle <= 90) || (originalAngle > 180 && originalAngle <= 270)) { |
|---|
| 780 | w = (int) (iw * Math.sin(DEGREE_90 - radian) + ih * Math.sin(radian)); |
|---|
| 781 | h = (int) (iw * Math.sin(radian) + ih * Math.sin(DEGREE_90 - radian)); |
|---|
| 782 | } else { |
|---|
| 783 | w = (int) (ih * Math.sin(DEGREE_90 - radian) + iw * Math.sin(radian)); |
|---|
| 784 | h = (int) (ih * Math.sin(radian) + iw * Math.sin(DEGREE_90 - radian)); |
|---|
| 785 | } |
|---|
| 786 | BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); |
|---|
| 787 | Graphics g = image.getGraphics(); |
|---|
| 788 | Graphics2D g2d = (Graphics2D) g.create(); |
|---|
| 789 | |
|---|
| 790 | // calculate the center of the icon. |
|---|
| 791 | int cx = iw / 2; |
|---|
| 792 | int cy = ih / 2; |
|---|
| 793 | |
|---|
| 794 | // move the graphics center point to the center of the icon. |
|---|
| 795 | g2d.translate(w / 2, h / 2); |
|---|
| 796 | |
|---|
| 797 | // rotate the graphics about the center point of the icon |
|---|
| 798 | g2d.rotate(Math.toRadians(originalAngle)); |
|---|
| 799 | |
|---|
| 800 | g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); |
|---|
| 801 | g2d.drawImage(img, -cx, -cy, c); |
|---|
| 802 | |
|---|
| 803 | g2d.dispose(); |
|---|
| 804 | new ImageIcon(image); // load completely |
|---|
| 805 | return image; |
|---|
| 806 | } |
|---|
| 807 | |
|---|
| 808 | /** |
|---|
| 809 | * Replies the icon for an OSM primitive type |
|---|
| 810 | * @param type the type |
|---|
| 811 | * @return the icon |
|---|
| 812 | */ |
|---|
| 813 | public static ImageIcon get(OsmPrimitiveType type) throws IllegalArgumentException { |
|---|
| 814 | CheckParameterUtil.ensureParameterNotNull(type, "type"); |
|---|
| 815 | return get("data", type.getAPIName()); |
|---|
| 816 | } |
|---|
| 817 | |
|---|
| 818 | public static BufferedImage createImageFromSvg(SVGDiagram svg, Dimension dim) { |
|---|
| 819 | float realWidth = svg.getWidth(); |
|---|
| 820 | float realHeight = svg.getHeight(); |
|---|
| 821 | int width = Math.round(realWidth); |
|---|
| 822 | int height = Math.round(realHeight); |
|---|
| 823 | Double scaleX = null, scaleY = null; |
|---|
| 824 | if (dim.width != -1) { |
|---|
| 825 | width = dim.width; |
|---|
| 826 | scaleX = (double) width / realWidth; |
|---|
| 827 | if (dim.height == -1) { |
|---|
| 828 | scaleY = scaleX; |
|---|
| 829 | height = (int) Math.round(realHeight * scaleY); |
|---|
| 830 | } else { |
|---|
| 831 | height = dim.height; |
|---|
| 832 | scaleY = (double) height / realHeight; |
|---|
| 833 | } |
|---|
| 834 | } else if (dim.height != -1) { |
|---|
| 835 | height = dim.height; |
|---|
| 836 | scaleX = scaleY = (double) height / realHeight; |
|---|
| 837 | width = (int) Math.round(realWidth * scaleX); |
|---|
| 838 | } |
|---|
| 839 | BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); |
|---|
| 840 | Graphics2D g = img.createGraphics(); |
|---|
| 841 | g.setClip(0, 0, width, height); |
|---|
| 842 | if (scaleX != null) { |
|---|
| 843 | g.scale(scaleX, scaleY); |
|---|
| 844 | } |
|---|
| 845 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
|---|
| 846 | try { |
|---|
| 847 | svg.render(g); |
|---|
| 848 | } catch (SVGException ex) { |
|---|
| 849 | return null; |
|---|
| 850 | } |
|---|
| 851 | return img; |
|---|
| 852 | } |
|---|
| 853 | |
|---|
| 854 | private static SVGUniverse getSvgUniverse() { |
|---|
| 855 | if (svgUniverse == null) { |
|---|
| 856 | svgUniverse = new SVGUniverse(); |
|---|
| 857 | } |
|---|
| 858 | return svgUniverse; |
|---|
| 859 | } |
|---|
| 860 | } |
|---|