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    }