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