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