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 }