001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Cursor; 007import java.awt.Dimension; 008import java.awt.Graphics; 009import java.awt.Graphics2D; 010import java.awt.GraphicsConfiguration; 011import java.awt.GraphicsEnvironment; 012import java.awt.Image; 013import java.awt.Point; 014import java.awt.RenderingHints; 015import java.awt.Toolkit; 016import java.awt.Transparency; 017import java.awt.image.BufferedImage; 018import java.io.ByteArrayInputStream; 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.StringReader; 023import java.io.UnsupportedEncodingException; 024import java.net.MalformedURLException; 025import java.net.URI; 026import java.net.URL; 027import java.net.URLDecoder; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.Collection; 031import java.util.HashMap; 032import java.util.Map; 033import java.util.concurrent.ExecutorService; 034import java.util.concurrent.Executors; 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037import java.util.zip.ZipEntry; 038import java.util.zip.ZipFile; 039 040import javax.imageio.ImageIO; 041import javax.swing.Icon; 042import javax.swing.ImageIcon; 043 044import org.apache.commons.codec.binary.Base64; 045import org.openstreetmap.josm.Main; 046import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 047import org.openstreetmap.josm.io.MirroredInputStream; 048import org.openstreetmap.josm.plugins.PluginHandler; 049import org.xml.sax.Attributes; 050import org.xml.sax.EntityResolver; 051import org.xml.sax.InputSource; 052import org.xml.sax.SAXException; 053import org.xml.sax.XMLReader; 054import org.xml.sax.helpers.DefaultHandler; 055import org.xml.sax.helpers.XMLReaderFactory; 056 057import com.kitfox.svg.SVGDiagram; 058import com.kitfox.svg.SVGException; 059import com.kitfox.svg.SVGUniverse; 060 061/** 062 * Helper class to support the application with images. 063 * 064 * How to use: 065 * 066 * <code>ImageIcon icon = new ImageProvider(name).setMaxWidth(24).setMaxHeight(24).get();</code> 067 * (there are more options, see below) 068 * 069 * short form: 070 * <code>ImageIcon icon = ImageProvider.get(name);</code> 071 * 072 * @author imi 073 */ 074public class ImageProvider { 075 076 /** 077 * Position of an overlay icon 078 * @author imi 079 */ 080 public static enum OverlayPosition { 081 NORTHWEST, NORTHEAST, SOUTHWEST, SOUTHEAST 082 } 083 084 /** 085 * Supported image types 086 */ 087 public static enum ImageType { 088 /** Scalable vector graphics */ 089 SVG, 090 /** Everything else, e.g. png, gif (must be supported by Java) */ 091 OTHER 092 } 093 094 protected Collection<String> dirs; 095 protected String id; 096 protected String subdir; 097 protected String name; 098 protected File archive; 099 protected String inArchiveDir; 100 protected int width = -1; 101 protected int height = -1; 102 protected int maxWidth = -1; 103 protected int maxHeight = -1; 104 protected boolean optional; 105 protected boolean suppressWarnings; 106 protected Collection<ClassLoader> additionalClassLoaders; 107 108 private static SVGUniverse svgUniverse; 109 110 /** 111 * The icon cache 112 */ 113 private static final Map<String, ImageResource> cache = new HashMap<String, ImageResource>(); 114 115 /** 116 * Caches the image data for rotated versions of the same image. 117 */ 118 private static final Map<Image, Map<Long, ImageResource>> ROTATE_CACHE = new HashMap<Image, Map<Long, ImageResource>>(); 119 120 private static final ExecutorService IMAGE_FETCHER = Executors.newSingleThreadExecutor(); 121 122 public interface ImageCallback { 123 void finished(ImageIcon result); 124 } 125 126 /** 127 * Constructs a new {@code ImageProvider} from a filename in a given directory. 128 * @param subdir subdirectory the image lies in 129 * @param name the name of the image. If it does not end with '.png' or '.svg', 130 * both extensions are tried. 131 */ 132 public ImageProvider(String subdir, String name) { 133 this.subdir = subdir; 134 this.name = name; 135 } 136 137 /** 138 * Constructs a new {@code ImageProvider} from a filename. 139 * @param name the name of the image. If it does not end with '.png' or '.svg', 140 * both extensions are tried. 141 */ 142 public ImageProvider(String name) { 143 this.name = name; 144 } 145 146 /** 147 * Directories to look for the image. 148 * @param dirs The directories to look for. 149 * @return the current object, for convenience 150 */ 151 public ImageProvider setDirs(Collection<String> dirs) { 152 this.dirs = dirs; 153 return this; 154 } 155 156 /** 157 * Set an id used for caching. 158 * If name starts with <tt>http://</tt> Id is not used for the cache. 159 * (A URL is unique anyway.) 160 * @return the current object, for convenience 161 */ 162 public ImageProvider setId(String id) { 163 this.id = id; 164 return this; 165 } 166 167 /** 168 * Specify a zip file where the image is located. 169 * 170 * (optional) 171 * @return the current object, for convenience 172 */ 173 public ImageProvider setArchive(File archive) { 174 this.archive = archive; 175 return this; 176 } 177 178 /** 179 * Specify a base path inside the zip file. 180 * 181 * The subdir and name will be relative to this path. 182 * 183 * (optional) 184 * @return the current object, for convenience 185 */ 186 public ImageProvider setInArchiveDir(String inArchiveDir) { 187 this.inArchiveDir = inArchiveDir; 188 return this; 189 } 190 191 /** 192 * Set the dimensions of the image. 193 * 194 * If not specified, the original size of the image is used. 195 * The width part of the dimension can be -1. Then it will only set the height but 196 * keep the aspect ratio. (And the other way around.) 197 * @return the current object, for convenience 198 */ 199 public ImageProvider setSize(Dimension size) { 200 this.width = size.width; 201 this.height = size.height; 202 return this; 203 } 204 205 /** 206 * @see #setSize 207 * @return the current object, for convenience 208 */ 209 public ImageProvider setWidth(int width) { 210 this.width = width; 211 return this; 212 } 213 214 /** 215 * @see #setSize 216 * @return the current object, for convenience 217 */ 218 public ImageProvider setHeight(int height) { 219 this.height = height; 220 return this; 221 } 222 223 /** 224 * Limit the maximum size of the image. 225 * 226 * It will shrink the image if necessary, but keep the aspect ratio. 227 * The given width or height can be -1 which means this direction is not bounded. 228 * 229 * 'size' and 'maxSize' are not compatible, you should set only one of them. 230 * @return the current object, for convenience 231 */ 232 public ImageProvider setMaxSize(Dimension maxSize) { 233 this.maxWidth = maxSize.width; 234 this.maxHeight = maxSize.height; 235 return this; 236 } 237 238 /** 239 * Convenience method, see {@link #setMaxSize(Dimension)}. 240 * @return the current object, for convenience 241 */ 242 public ImageProvider setMaxSize(int maxSize) { 243 return this.setMaxSize(new Dimension(maxSize, maxSize)); 244 } 245 246 /** 247 * @see #setMaxSize 248 * @return the current object, for convenience 249 */ 250 public ImageProvider setMaxWidth(int maxWidth) { 251 this.maxWidth = maxWidth; 252 return this; 253 } 254 255 /** 256 * @see #setMaxSize 257 * @return the current object, for convenience 258 */ 259 public ImageProvider setMaxHeight(int maxHeight) { 260 this.maxHeight = maxHeight; 261 return this; 262 } 263 264 /** 265 * Decide, if an exception should be thrown, when the image cannot be located. 266 * 267 * Set to true, when the image URL comes from user data and the image may be missing. 268 * 269 * @param optional true, if JOSM should <b>not</b> throw a RuntimeException 270 * in case the image cannot be located. 271 * @return the current object, for convenience 272 */ 273 public ImageProvider setOptional(boolean optional) { 274 this.optional = optional; 275 return this; 276 } 277 278 /** 279 * Suppresses warning on the command line in case the image cannot be found. 280 * 281 * In combination with setOptional(true); 282 * @return the current object, for convenience 283 */ 284 public ImageProvider setSuppressWarnings(boolean suppressWarnings) { 285 this.suppressWarnings = suppressWarnings; 286 return this; 287 } 288 289 /** 290 * Add a collection of additional class loaders to search image for. 291 * @return the current object, for convenience 292 */ 293 public ImageProvider setAdditionalClassLoaders(Collection<ClassLoader> additionalClassLoaders) { 294 this.additionalClassLoaders = additionalClassLoaders; 295 return this; 296 } 297 298 /** 299 * Execute the image request. 300 * @return the requested image or null if the request failed 301 */ 302 public ImageIcon get() { 303 ImageResource ir = getIfAvailableImpl(additionalClassLoaders); 304 if (ir == null) { 305 if (!optional) { 306 String ext = name.indexOf('.') != -1 ? "" : ".???"; 307 throw new RuntimeException(tr("Fatal: failed to locate image ''{0}''. This is a serious configuration problem. JOSM will stop working.", name + ext)); 308 } else { 309 if (!suppressWarnings) { 310 Main.error(tr("Failed to locate image ''{0}''", name)); 311 } 312 return null; 313 } 314 } 315 if (maxWidth != -1 || maxHeight != -1) 316 return ir.getImageIconBounded(new Dimension(maxWidth, maxHeight)); 317 else 318 return ir.getImageIcon(new Dimension(width, height)); 319 } 320 321 /** 322 * Load the image in a background thread. 323 * 324 * This method returns immediately and runs the image request 325 * asynchronously. 326 * 327 * @param callback a callback. It is called, when the image is ready. 328 * This can happen before the call to this method returns or it may be 329 * invoked some time (seconds) later. If no image is available, a null 330 * value is returned to callback (just like {@link #get}). 331 */ 332 public void getInBackground(final ImageCallback callback) { 333 if (name.startsWith("http://") || name.startsWith("wiki://")) { 334 Runnable fetch = new Runnable() { 335 @Override 336 public void run() { 337 ImageIcon result = get(); 338 callback.finished(result); 339 } 340 }; 341 IMAGE_FETCHER.submit(fetch); 342 } else { 343 ImageIcon result = get(); 344 callback.finished(result); 345 } 346 } 347 348 /** 349 * Load an image with a given file name. 350 * 351 * @param subdir subdirectory the image lies in 352 * @param name The icon name (base name with or without '.png' or '.svg' extension) 353 * @return The requested Image. 354 * @throws RuntimeException if the image cannot be located 355 */ 356 public static ImageIcon get(String subdir, String name) { 357 return new ImageProvider(subdir, name).get(); 358 } 359 360 /** 361 * @see #get(java.lang.String, java.lang.String) 362 */ 363 public static ImageIcon get(String name) { 364 return new ImageProvider(name).get(); 365 } 366 367 /** 368 * Load an image with a given file name, but do not throw an exception 369 * when the image cannot be found. 370 * @see #get(java.lang.String, java.lang.String) 371 */ 372 public static ImageIcon getIfAvailable(String subdir, String name) { 373 return new ImageProvider(subdir, name).setOptional(true).get(); 374 } 375 376 /** 377 * @see #getIfAvailable(java.lang.String, java.lang.String) 378 */ 379 public static ImageIcon getIfAvailable(String name) { 380 return new ImageProvider(name).setOptional(true).get(); 381 } 382 383 /** 384 * {@code data:[<mediatype>][;base64],<data>} 385 * @see <a href="http://tools.ietf.org/html/rfc2397">RFC2397</a> 386 */ 387 private static final Pattern dataUrlPattern = Pattern.compile( 388 "^data:([a-zA-Z]+/[a-zA-Z+]+)?(;base64)?,(.+)$"); 389 390 private ImageResource getIfAvailableImpl(Collection<ClassLoader> additionalClassLoaders) { 391 synchronized (cache) { 392 // This method is called from different thread and modifying HashMap concurrently can result 393 // for example in loops in map entries (ie freeze when such entry is retrieved) 394 // Yes, it did happen to me :-) 395 if (name == null) 396 return null; 397 398 try { 399 if (name.startsWith("data:")) { 400 Matcher m = dataUrlPattern.matcher(name); 401 if (m.matches()) { 402 String mediatype = m.group(1); 403 String base64 = m.group(2); 404 String data = m.group(3); 405 byte[] bytes = ";base64".equals(base64) 406 ? Base64.decodeBase64(data) 407 : URLDecoder.decode(data, "utf-8").getBytes(); 408 if (mediatype != null && mediatype.contains("image/svg+xml")) { 409 URI uri = getSvgUniverse().loadSVG(new StringReader(new String(bytes)), name); 410 return new ImageResource(getSvgUniverse().getDiagram(uri)); 411 } else { 412 try { 413 return new ImageResource(ImageIO.read(new ByteArrayInputStream(bytes))); 414 } catch (IOException e) { 415 Main.warn("IOException while reading image: "+e.getMessage()); 416 } 417 } 418 } 419 } 420 } catch (UnsupportedEncodingException ex) { 421 throw new RuntimeException(ex.getMessage(), ex); 422 } 423 424 ImageType type = name.toLowerCase().endsWith(".svg") ? ImageType.SVG : ImageType.OTHER; 425 426 if (name.startsWith("http://")) { 427 String url = name; 428 ImageResource ir = cache.get(url); 429 if (ir != null) return ir; 430 ir = getIfAvailableHttp(url, type); 431 if (ir != null) { 432 cache.put(url, ir); 433 } 434 return ir; 435 } else if (name.startsWith("wiki://")) { 436 ImageResource ir = cache.get(name); 437 if (ir != null) return ir; 438 ir = getIfAvailableWiki(name, type); 439 if (ir != null) { 440 cache.put(name, ir); 441 } 442 return ir; 443 } 444 445 if (subdir == null) { 446 subdir = ""; 447 } else if (!subdir.isEmpty()) { 448 subdir += "/"; 449 } 450 String[] extensions; 451 if (name.indexOf('.') != -1) { 452 extensions = new String[] { "" }; 453 } else { 454 extensions = new String[] { ".png", ".svg"}; 455 } 456 final int ARCHIVE = 0, LOCAL = 1; 457 for (int place : new Integer[] { ARCHIVE, LOCAL }) { 458 for (String ext : extensions) { 459 460 if (".svg".equals(ext)) { 461 type = ImageType.SVG; 462 } else if (".png".equals(ext)) { 463 type = ImageType.OTHER; 464 } 465 466 String full_name = subdir + name + ext; 467 String cache_name = full_name; 468 /* cache separately */ 469 if (dirs != null && !dirs.isEmpty()) { 470 cache_name = "id:" + id + ":" + full_name; 471 if(archive != null) { 472 cache_name += ":" + archive.getName(); 473 } 474 } 475 476 ImageResource ir = cache.get(cache_name); 477 if (ir != null) return ir; 478 479 switch (place) { 480 case ARCHIVE: 481 if (archive != null) { 482 ir = getIfAvailableZip(full_name, archive, inArchiveDir, type); 483 if (ir != null) { 484 cache.put(cache_name, ir); 485 return ir; 486 } 487 } 488 break; 489 case LOCAL: 490 // getImageUrl() does a ton of "stat()" calls and gets expensive 491 // and redundant when you have a whole ton of objects. So, 492 // index the cache by the name of the icon we're looking for 493 // and don't bother to create a URL unless we're actually 494 // creating the image. 495 URL path = getImageUrl(full_name, dirs, additionalClassLoaders); 496 if (path == null) { 497 continue; 498 } 499 ir = getIfAvailableLocalURL(path, type); 500 if (ir != null) { 501 cache.put(cache_name, ir); 502 return ir; 503 } 504 break; 505 } 506 } 507 } 508 return null; 509 } 510 } 511 512 private static ImageResource getIfAvailableHttp(String url, ImageType type) { 513 MirroredInputStream is = null; 514 try { 515 is = new MirroredInputStream(url, 516 new File(Main.pref.getCacheDirectory(), "images").getPath()); 517 switch (type) { 518 case SVG: 519 URI uri = getSvgUniverse().loadSVG(is, is.getFile().toURI().toURL().toString()); 520 SVGDiagram svg = getSvgUniverse().getDiagram(uri); 521 return svg == null ? null : new ImageResource(svg); 522 case OTHER: 523 BufferedImage img = null; 524 try { 525 img = ImageIO.read(is.getFile().toURI().toURL()); 526 } catch (IOException e) { 527 Main.warn("IOException while reading HTTP image: "+e.getMessage()); 528 } 529 return img == null ? null : new ImageResource(img); 530 default: 531 throw new AssertionError(); 532 } 533 } catch (IOException e) { 534 return null; 535 } finally { 536 Utils.close(is); 537 } 538 } 539 540 private static ImageResource getIfAvailableWiki(String name, ImageType type) { 541 final Collection<String> defaultBaseUrls = Arrays.asList( 542 "http://wiki.openstreetmap.org/w/images/", 543 "http://upload.wikimedia.org/wikipedia/commons/", 544 "http://wiki.openstreetmap.org/wiki/File:" 545 ); 546 final Collection<String> baseUrls = Main.pref.getCollection("image-provider.wiki.urls", defaultBaseUrls); 547 548 final String fn = name.substring(name.lastIndexOf('/') + 1); 549 550 ImageResource result = null; 551 for (String b : baseUrls) { 552 String url; 553 if (b.endsWith(":")) { 554 url = getImgUrlFromWikiInfoPage(b, fn); 555 if (url == null) { 556 continue; 557 } 558 } else { 559 final String fn_md5 = Utils.md5Hex(fn); 560 url = b + fn_md5.substring(0,1) + "/" + fn_md5.substring(0,2) + "/" + fn; 561 } 562 result = getIfAvailableHttp(url, type); 563 if (result != null) { 564 break; 565 } 566 } 567 return result; 568 } 569 570 private static ImageResource getIfAvailableZip(String full_name, File archive, String inArchiveDir, ImageType type) { 571 ZipFile zipFile = null; 572 try 573 { 574 zipFile = new ZipFile(archive); 575 if (inArchiveDir == null || inArchiveDir.equals(".")) { 576 inArchiveDir = ""; 577 } else if (!inArchiveDir.isEmpty()) { 578 inArchiveDir += "/"; 579 } 580 String entry_name = inArchiveDir + full_name; 581 ZipEntry entry = zipFile.getEntry(entry_name); 582 if(entry != null) 583 { 584 int size = (int)entry.getSize(); 585 int offs = 0; 586 byte[] buf = new byte[size]; 587 InputStream is = null; 588 try { 589 is = zipFile.getInputStream(entry); 590 switch (type) { 591 case SVG: 592 URI uri = getSvgUniverse().loadSVG(is, entry_name); 593 SVGDiagram svg = getSvgUniverse().getDiagram(uri); 594 return svg == null ? null : new ImageResource(svg); 595 case OTHER: 596 while(size > 0) 597 { 598 int l = is.read(buf, offs, size); 599 offs += l; 600 size -= l; 601 } 602 BufferedImage img = null; 603 try { 604 img = ImageIO.read(new ByteArrayInputStream(buf)); 605 } catch (IOException e) { 606 Main.warn(e); 607 } 608 return img == null ? null : new ImageResource(img); 609 default: 610 throw new AssertionError(); 611 } 612 } finally { 613 Utils.close(is); 614 } 615 } 616 } catch (Exception e) { 617 Main.warn(tr("Failed to handle zip file ''{0}''. Exception was: {1}", archive.getName(), e.toString())); 618 } finally { 619 Utils.close(zipFile); 620 } 621 return null; 622 } 623 624 private static ImageResource getIfAvailableLocalURL(URL path, ImageType type) { 625 switch (type) { 626 case SVG: 627 URI uri = getSvgUniverse().loadSVG(path); 628 SVGDiagram svg = getSvgUniverse().getDiagram(uri); 629 return svg == null ? null : new ImageResource(svg); 630 case OTHER: 631 BufferedImage img = null; 632 try { 633 img = ImageIO.read(path); 634 } catch (IOException e) { 635 Main.warn(e); 636 } 637 return img == null ? null : new ImageResource(img); 638 default: 639 throw new AssertionError(); 640 } 641 } 642 643 private static URL getImageUrl(String path, String name, Collection<ClassLoader> additionalClassLoaders) { 644 if (path != null && path.startsWith("resource://")) { 645 String p = path.substring("resource://".length()); 646 Collection<ClassLoader> classLoaders = new ArrayList<ClassLoader>(PluginHandler.getResourceClassLoaders()); 647 if (additionalClassLoaders != null) { 648 classLoaders.addAll(additionalClassLoaders); 649 } 650 for (ClassLoader source : classLoaders) { 651 URL res; 652 if ((res = source.getResource(p + name)) != null) 653 return res; 654 } 655 } else { 656 try { 657 File f = new File(path, name); 658 if ((path != null || f.isAbsolute()) && f.exists()) 659 return f.toURI().toURL(); 660 } catch (MalformedURLException e) { 661 Main.warn(e); 662 } 663 } 664 return null; 665 } 666 667 private static URL getImageUrl(String imageName, Collection<String> dirs, Collection<ClassLoader> additionalClassLoaders) { 668 URL u = null; 669 670 // Try passed directories first 671 if (dirs != null) { 672 for (String name : dirs) { 673 try { 674 u = getImageUrl(name, imageName, additionalClassLoaders); 675 if (u != null) 676 return u; 677 } catch (SecurityException e) { 678 Main.warn(tr( 679 "Failed to access directory ''{0}'' for security reasons. Exception was: {1}", 680 name, e.toString())); 681 } 682 683 } 684 } 685 // Try user-preference directory 686 String dir = Main.pref.getPreferencesDir() + "images"; 687 try { 688 u = getImageUrl(dir, imageName, additionalClassLoaders); 689 if (u != null) 690 return u; 691 } catch (SecurityException e) { 692 Main.warn(tr( 693 "Failed to access directory ''{0}'' for security reasons. Exception was: {1}", dir, e 694 .toString())); 695 } 696 697 // Absolute path? 698 u = getImageUrl(null, imageName, additionalClassLoaders); 699 if (u != null) 700 return u; 701 702 // Try plugins and josm classloader 703 u = getImageUrl("resource://images/", imageName, additionalClassLoaders); 704 if (u != null) 705 return u; 706 707 // Try all other resource directories 708 for (String location : Main.pref.getAllPossiblePreferenceDirs()) { 709 u = getImageUrl(location + "images", imageName, additionalClassLoaders); 710 if (u != null) 711 return u; 712 u = getImageUrl(location, imageName, additionalClassLoaders); 713 if (u != null) 714 return u; 715 } 716 717 return null; 718 } 719 720 /** 721 * Reads the wiki page on a certain file in html format in order to find the real image URL. 722 */ 723 private static String getImgUrlFromWikiInfoPage(final String base, final String fn) { 724 725 /** Quit parsing, when a certain condition is met */ 726 class SAXReturnException extends SAXException { 727 private String result; 728 729 public SAXReturnException(String result) { 730 this.result = result; 731 } 732 733 public String getResult() { 734 return result; 735 } 736 } 737 738 try { 739 final XMLReader parser = XMLReaderFactory.createXMLReader(); 740 parser.setContentHandler(new DefaultHandler() { 741 @Override 742 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { 743 if (localName.equalsIgnoreCase("img")) { 744 String val = atts.getValue("src"); 745 if (val.endsWith(fn)) 746 throw new SAXReturnException(val); // parsing done, quit early 747 } 748 } 749 }); 750 751 parser.setEntityResolver(new EntityResolver() { 752 @Override 753 public InputSource resolveEntity (String publicId, String systemId) { 754 return new InputSource(new ByteArrayInputStream(new byte[0])); 755 } 756 }); 757 758 parser.parse(new InputSource(new MirroredInputStream( 759 base + fn, 760 new File(Main.pref.getPreferencesDir(), "images").toString() 761 ))); 762 } catch (SAXReturnException r) { 763 return r.getResult(); 764 } catch (Exception e) { 765 Main.warn("Parsing " + base + fn + " failed:\n" + e); 766 return null; 767 } 768 Main.warn("Parsing " + base + fn + " failed: Unexpected content."); 769 return null; 770 } 771 772 public static Cursor getCursor(String name, String overlay) { 773 ImageIcon img = get("cursor", name); 774 if (overlay != null) { 775 img = overlay(img, ImageProvider.get("cursor/modifier/" + overlay), OverlayPosition.SOUTHEAST); 776 } 777 Cursor c = Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(), 778 name.equals("crosshair") ? new Point(10, 10) : new Point(3, 2), "Cursor"); 779 return c; 780 } 781 782 /** 783 * Decorate one icon with an overlay icon. 784 * 785 * @param ground the base image 786 * @param overlay the overlay image (can be smaller than the base image) 787 * @param pos position of the overlay image inside the base image (positioned 788 * in one of the corners) 789 * @return an icon that represent the overlay of the two given icons. The second icon is layed 790 * on the first relative to the given position. 791 */ 792 public static ImageIcon overlay(Icon ground, Icon overlay, OverlayPosition pos) { 793 GraphicsConfiguration conf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice() 794 .getDefaultConfiguration(); 795 int w = ground.getIconWidth(); 796 int h = ground.getIconHeight(); 797 int wo = overlay.getIconWidth(); 798 int ho = overlay.getIconHeight(); 799 BufferedImage img = conf.createCompatibleImage(w, h, Transparency.TRANSLUCENT); 800 Graphics g = img.createGraphics(); 801 ground.paintIcon(null, g, 0, 0); 802 int x = 0, y = 0; 803 switch (pos) { 804 case NORTHWEST: 805 x = 0; 806 y = 0; 807 break; 808 case NORTHEAST: 809 x = w - wo; 810 y = 0; 811 break; 812 case SOUTHWEST: 813 x = 0; 814 y = h - ho; 815 break; 816 case SOUTHEAST: 817 x = w - wo; 818 y = h - ho; 819 break; 820 } 821 overlay.paintIcon(null, g, x, y); 822 return new ImageIcon(img); 823 } 824 825 /** 90 degrees in radians units */ 826 final static double DEGREE_90 = 90.0 * Math.PI / 180.0; 827 828 /** 829 * Creates a rotated version of the input image. 830 * 831 * @param img the image to be rotated. 832 * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we 833 * will mod it with 360 before using it. More over for caching performance, it will be rounded to 834 * an entire value between 0 and 360. 835 * 836 * @return the image after rotating. 837 * @since 6172 838 */ 839 public static Image createRotatedImage(Image img, double rotatedAngle) { 840 return createRotatedImage(img, rotatedAngle, ImageResource.DEFAULT_DIMENSION); 841 } 842 843 /** 844 * Creates a rotated version of the input image, scaled to the given dimension. 845 * 846 * @param img the image to be rotated. 847 * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we 848 * will mod it with 360 before using it. More over for caching performance, it will be rounded to 849 * an entire value between 0 and 360. 850 * @param dimension The requested dimensions. Use (-1,-1) for the original size 851 * and (width, -1) to set the width, but otherwise scale the image proportionally. 852 * @return the image after rotating and scaling. 853 * @since 6172 854 */ 855 public static Image createRotatedImage(Image img, double rotatedAngle, Dimension dimension) { 856 CheckParameterUtil.ensureParameterNotNull(img, "img"); 857 858 // convert rotatedAngle to an integer value from 0 to 360 859 Long originalAngle = Math.round(rotatedAngle % 360); 860 if (rotatedAngle != 0 && originalAngle == 0) { 861 originalAngle = 360L; 862 } 863 864 ImageResource imageResource = null; 865 866 synchronized (ROTATE_CACHE) { 867 Map<Long, ImageResource> cacheByAngle = ROTATE_CACHE.get(img); 868 if (cacheByAngle == null) { 869 ROTATE_CACHE.put(img, cacheByAngle = new HashMap<Long, ImageResource>()); 870 } 871 872 imageResource = cacheByAngle.get(originalAngle); 873 874 if (imageResource == null) { 875 // convert originalAngle to a value from 0 to 90 876 double angle = originalAngle % 90; 877 if (originalAngle != 0.0 && angle == 0.0) { 878 angle = 90.0; 879 } 880 881 double radian = Math.toRadians(angle); 882 883 new ImageIcon(img); // load completely 884 int iw = img.getWidth(null); 885 int ih = img.getHeight(null); 886 int w; 887 int h; 888 889 if ((originalAngle >= 0 && originalAngle <= 90) || (originalAngle > 180 && originalAngle <= 270)) { 890 w = (int) (iw * Math.sin(DEGREE_90 - radian) + ih * Math.sin(radian)); 891 h = (int) (iw * Math.sin(radian) + ih * Math.sin(DEGREE_90 - radian)); 892 } else { 893 w = (int) (ih * Math.sin(DEGREE_90 - radian) + iw * Math.sin(radian)); 894 h = (int) (ih * Math.sin(radian) + iw * Math.sin(DEGREE_90 - radian)); 895 } 896 Image image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); 897 cacheByAngle.put(originalAngle, imageResource = new ImageResource(image)); 898 Graphics g = image.getGraphics(); 899 Graphics2D g2d = (Graphics2D) g.create(); 900 901 // calculate the center of the icon. 902 int cx = iw / 2; 903 int cy = ih / 2; 904 905 // move the graphics center point to the center of the icon. 906 g2d.translate(w / 2, h / 2); 907 908 // rotate the graphics about the center point of the icon 909 g2d.rotate(Math.toRadians(originalAngle)); 910 911 g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); 912 g2d.drawImage(img, -cx, -cy, null); 913 914 g2d.dispose(); 915 new ImageIcon(image); // load completely 916 } 917 return imageResource.getImageIcon(dimension).getImage(); 918 } 919 } 920 921 /** 922 * Creates a scaled down version of the input image to fit maximum dimensions. (Keeps aspect ratio) 923 * 924 * @param img the image to be scaled down. 925 * @param maxSize the maximum size in pixels (both for width and height) 926 * 927 * @return the image after scaling. 928 * @since 6172 929 */ 930 public static Image createBoundedImage(Image img, int maxSize) { 931 return new ImageResource(img).getImageIconBounded(new Dimension(maxSize, maxSize)).getImage(); 932 } 933 934 /** 935 * Replies the icon for an OSM primitive type 936 * @param type the type 937 * @return the icon 938 */ 939 public static ImageIcon get(OsmPrimitiveType type) { 940 CheckParameterUtil.ensureParameterNotNull(type, "type"); 941 return get("data", type.getAPIName()); 942 } 943 944 public static BufferedImage createImageFromSvg(SVGDiagram svg, Dimension dim) { 945 float realWidth = svg.getWidth(); 946 float realHeight = svg.getHeight(); 947 int width = Math.round(realWidth); 948 int height = Math.round(realHeight); 949 Double scaleX = null, scaleY = null; 950 if (dim.width != -1) { 951 width = dim.width; 952 scaleX = (double) width / realWidth; 953 if (dim.height == -1) { 954 scaleY = scaleX; 955 height = (int) Math.round(realHeight * scaleY); 956 } else { 957 height = dim.height; 958 scaleY = (double) height / realHeight; 959 } 960 } else if (dim.height != -1) { 961 height = dim.height; 962 scaleX = scaleY = (double) height / realHeight; 963 width = (int) Math.round(realWidth * scaleX); 964 } 965 if (width == 0 || height == 0) { 966 return null; 967 } 968 BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 969 Graphics2D g = img.createGraphics(); 970 g.setClip(0, 0, width, height); 971 if (scaleX != null && scaleY != null) { 972 g.scale(scaleX, scaleY); 973 } 974 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 975 try { 976 svg.render(g); 977 } catch (SVGException ex) { 978 return null; 979 } 980 return img; 981 } 982 983 private static SVGUniverse getSvgUniverse() { 984 if (svgUniverse == null) { 985 svgUniverse = new SVGUniverse(); 986 } 987 return svgUniverse; 988 } 989}