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    }