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