source: josm/trunk/src/org/openstreetmap/josm/tools/ImageProvider.java@ 6934

Last change on this file since 6934 was 6920, checked in by Don-vip, 10 years ago

fix #9778, fix #9806 - access OSM API and JOSM website in HTTPS by default + other HTTPS links where applicable + update CONTRIBUTION

  • Property svn:eol-style set to native
File size: 36.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Cursor;
7import java.awt.Dimension;
8import java.awt.Graphics;
9import java.awt.Graphics2D;
10import java.awt.GraphicsConfiguration;
11import java.awt.GraphicsEnvironment;
12import java.awt.Image;
13import java.awt.Point;
14import java.awt.RenderingHints;
15import java.awt.Toolkit;
16import java.awt.Transparency;
17import java.awt.image.BufferedImage;
18import java.io.ByteArrayInputStream;
19import java.io.File;
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.StringReader;
23import java.io.UnsupportedEncodingException;
24import java.net.URI;
25import java.net.URL;
26import java.net.URLDecoder;
27import java.net.URLEncoder;
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.Collection;
31import java.util.HashMap;
32import java.util.Map;
33import java.util.concurrent.ExecutorService;
34import java.util.concurrent.Executors;
35import java.util.regex.Matcher;
36import java.util.regex.Pattern;
37import java.util.zip.ZipEntry;
38import java.util.zip.ZipFile;
39
40import javax.imageio.ImageIO;
41import javax.swing.Icon;
42import javax.swing.ImageIcon;
43
44import org.apache.commons.codec.binary.Base64;
45import org.openstreetmap.josm.Main;
46import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
47import org.openstreetmap.josm.io.MirroredInputStream;
48import org.openstreetmap.josm.plugins.PluginHandler;
49import org.xml.sax.Attributes;
50import org.xml.sax.EntityResolver;
51import org.xml.sax.InputSource;
52import org.xml.sax.SAXException;
53import org.xml.sax.XMLReader;
54import org.xml.sax.helpers.DefaultHandler;
55import org.xml.sax.helpers.XMLReaderFactory;
56
57import com.kitfox.svg.SVGDiagram;
58import com.kitfox.svg.SVGException;
59import com.kitfox.svg.SVGUniverse;
60
61/**
62 * Helper class to support the application with images.
63 *
64 * How to use:
65 *
66 * <code>ImageIcon icon = new ImageProvider(name).setMaxWidth(24).setMaxHeight(24).get();</code>
67 * (there are more options, see below)
68 *
69 * short form:
70 * <code>ImageIcon icon = ImageProvider.get(name);</code>
71 *
72 * @author imi
73 */
74public class ImageProvider {
75
76 /**
77 * Position of an overlay icon
78 * @author imi
79 */
80 public static enum OverlayPosition {
81 NORTHWEST, NORTHEAST, SOUTHWEST, SOUTHEAST
82 }
83
84 /**
85 * Supported image types
86 */
87 public static enum ImageType {
88 /** Scalable vector graphics */
89 SVG,
90 /** Everything else, e.g. png, gif (must be supported by Java) */
91 OTHER
92 }
93
94 protected Collection<String> dirs;
95 protected String id;
96 protected String subdir;
97 protected String name;
98 protected File archive;
99 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 * @param name The icon name (base name with or without '.png' or '.svg' extension)
362 * @return the requested image or null if the request failed
363 * @see #get(String, String)
364 */
365 public static ImageIcon get(String name) {
366 return new ImageProvider(name).get();
367 }
368
369 /**
370 * Load an image with a given file name, but do not throw an exception
371 * when the image cannot be found.
372 *
373 * @param subdir subdirectory the image lies in
374 * @param name The icon name (base name with or without '.png' or '.svg' extension)
375 * @return the requested image or null if the request failed
376 * @see #get(String, String)
377 */
378 public static ImageIcon getIfAvailable(String subdir, String name) {
379 return new ImageProvider(subdir, name).setOptional(true).get();
380 }
381
382 /**
383 * @param name The icon name (base name with or without '.png' or '.svg' extension)
384 * @return the requested image or null if the request failed
385 * @see #getIfAvailable(String, String)
386 */
387 public static ImageIcon getIfAvailable(String name) {
388 return new ImageProvider(name).setOptional(true).get();
389 }
390
391 /**
392 * {@code data:[<mediatype>][;base64],<data>}
393 * @see <a href="http://tools.ietf.org/html/rfc2397">RFC2397</a>
394 */
395 private static final Pattern dataUrlPattern = Pattern.compile(
396 "^data:([a-zA-Z]+/[a-zA-Z+]+)?(;base64)?,(.+)$");
397
398 private ImageResource getIfAvailableImpl(Collection<ClassLoader> additionalClassLoaders) {
399 synchronized (cache) {
400 // This method is called from different thread and modifying HashMap concurrently can result
401 // for example in loops in map entries (ie freeze when such entry is retrieved)
402 // Yes, it did happen to me :-)
403 if (name == null)
404 return null;
405
406 if (name.startsWith("data:")) {
407 String url = name;
408 ImageResource ir = cache.get(url);
409 if (ir != null) return ir;
410 ir = getIfAvailableDataUrl(url);
411 if (ir != null) {
412 cache.put(url, ir);
413 }
414 return ir;
415 }
416
417 ImageType type = name.toLowerCase().endsWith(".svg") ? ImageType.SVG : ImageType.OTHER;
418
419 if (name.startsWith("http://") || name.startsWith("https://")) {
420 String url = name;
421 ImageResource ir = cache.get(url);
422 if (ir != null) return ir;
423 ir = getIfAvailableHttp(url, type);
424 if (ir != null) {
425 cache.put(url, ir);
426 }
427 return ir;
428 } else if (name.startsWith("wiki://")) {
429 ImageResource ir = cache.get(name);
430 if (ir != null) return ir;
431 ir = getIfAvailableWiki(name, type);
432 if (ir != null) {
433 cache.put(name, ir);
434 }
435 return ir;
436 }
437
438 if (subdir == null) {
439 subdir = "";
440 } else if (!subdir.isEmpty()) {
441 subdir += "/";
442 }
443 String[] extensions;
444 if (name.indexOf('.') != -1) {
445 extensions = new String[] { "" };
446 } else {
447 extensions = new String[] { ".png", ".svg"};
448 }
449 final int ARCHIVE = 0, LOCAL = 1;
450 for (int place : new Integer[] { ARCHIVE, LOCAL }) {
451 for (String ext : extensions) {
452
453 if (".svg".equals(ext)) {
454 type = ImageType.SVG;
455 } else if (".png".equals(ext)) {
456 type = ImageType.OTHER;
457 }
458
459 String full_name = subdir + name + ext;
460 String cache_name = full_name;
461 /* cache separately */
462 if (dirs != null && !dirs.isEmpty()) {
463 cache_name = "id:" + id + ":" + full_name;
464 if(archive != null) {
465 cache_name += ":" + archive.getName();
466 }
467 }
468
469 ImageResource ir = cache.get(cache_name);
470 if (ir != null) return ir;
471
472 switch (place) {
473 case ARCHIVE:
474 if (archive != null) {
475 ir = getIfAvailableZip(full_name, archive, inArchiveDir, type);
476 if (ir != null) {
477 cache.put(cache_name, ir);
478 return ir;
479 }
480 }
481 break;
482 case LOCAL:
483 // getImageUrl() does a ton of "stat()" calls and gets expensive
484 // and redundant when you have a whole ton of objects. So,
485 // index the cache by the name of the icon we're looking for
486 // and don't bother to create a URL unless we're actually
487 // creating the image.
488 URL path = getImageUrl(full_name, dirs, additionalClassLoaders);
489 if (path == null) {
490 continue;
491 }
492 ir = getIfAvailableLocalURL(path, type);
493 if (ir != null) {
494 cache.put(cache_name, ir);
495 return ir;
496 }
497 break;
498 }
499 }
500 }
501 return null;
502 }
503 }
504
505 private static ImageResource getIfAvailableHttp(String url, ImageType type) {
506 MirroredInputStream is = null;
507 try {
508 is = new MirroredInputStream(url,
509 new File(Main.pref.getCacheDirectory(), "images").getPath());
510 switch (type) {
511 case SVG:
512 URI uri = getSvgUniverse().loadSVG(is, Utils.fileToURL(is.getFile()).toString());
513 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
514 return svg == null ? null : new ImageResource(svg);
515 case OTHER:
516 BufferedImage img = null;
517 try {
518 img = ImageIO.read(Utils.fileToURL(is.getFile()));
519 } catch (IOException e) {
520 Main.warn("IOException while reading HTTP image: "+e.getMessage());
521 }
522 return img == null ? null : new ImageResource(img);
523 default:
524 throw new AssertionError();
525 }
526 } catch (IOException e) {
527 return null;
528 } finally {
529 Utils.close(is);
530 }
531 }
532
533 private static ImageResource getIfAvailableDataUrl(String url) {
534 try {
535 Matcher m = dataUrlPattern.matcher(url);
536 if (m.matches()) {
537 String mediatype = m.group(1);
538 String base64 = m.group(2);
539 String data = m.group(3);
540 byte[] bytes;
541 if (";base64".equals(base64)) {
542 bytes = Base64.decodeBase64(data);
543 } else {
544 try {
545 bytes = URLDecoder.decode(data, "utf-8").getBytes();
546 } catch (IllegalArgumentException ex) {
547 Main.warn("Unable to decode URL data part: "+ex.getMessage() + " (" + data + ")");
548 return null;
549 }
550 }
551 if (mediatype != null && mediatype.contains("image/svg+xml")) {
552 String s = new String(bytes);
553 URI uri = getSvgUniverse().loadSVG(new StringReader(s), URLEncoder.encode(s, "UTF-8"));
554 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
555 if (svg == null) {
556 Main.warn("Unable to process svg: "+s);
557 return null;
558 }
559 return new ImageResource(svg);
560 } else {
561 try {
562 return new ImageResource(ImageIO.read(new ByteArrayInputStream(bytes)));
563 } catch (IOException e) {
564 Main.warn("IOException while reading image: "+e.getMessage());
565 }
566 }
567 }
568 return null;
569 } catch (UnsupportedEncodingException ex) {
570 throw new RuntimeException(ex.getMessage(), ex);
571 }
572 }
573
574 private static ImageResource getIfAvailableWiki(String name, ImageType type) {
575 final Collection<String> defaultBaseUrls = Arrays.asList(
576 "http://wiki.openstreetmap.org/w/images/",
577 "http://upload.wikimedia.org/wikipedia/commons/",
578 "http://wiki.openstreetmap.org/wiki/File:"
579 );
580 final Collection<String> baseUrls = Main.pref.getCollection("image-provider.wiki.urls", defaultBaseUrls);
581
582 final String fn = name.substring(name.lastIndexOf('/') + 1);
583
584 ImageResource result = null;
585 for (String b : baseUrls) {
586 String url;
587 if (b.endsWith(":")) {
588 url = getImgUrlFromWikiInfoPage(b, fn);
589 if (url == null) {
590 continue;
591 }
592 } else {
593 final String fn_md5 = Utils.md5Hex(fn);
594 url = b + fn_md5.substring(0,1) + "/" + fn_md5.substring(0,2) + "/" + fn;
595 }
596 result = getIfAvailableHttp(url, type);
597 if (result != null) {
598 break;
599 }
600 }
601 return result;
602 }
603
604 private static ImageResource getIfAvailableZip(String full_name, File archive, String inArchiveDir, ImageType type) {
605 ZipFile zipFile = null;
606 try
607 {
608 zipFile = new ZipFile(archive);
609 if (inArchiveDir == null || inArchiveDir.equals(".")) {
610 inArchiveDir = "";
611 } else if (!inArchiveDir.isEmpty()) {
612 inArchiveDir += "/";
613 }
614 String entry_name = inArchiveDir + full_name;
615 ZipEntry entry = zipFile.getEntry(entry_name);
616 if(entry != null)
617 {
618 int size = (int)entry.getSize();
619 int offs = 0;
620 byte[] buf = new byte[size];
621 InputStream is = null;
622 try {
623 is = zipFile.getInputStream(entry);
624 switch (type) {
625 case SVG:
626 URI uri = getSvgUniverse().loadSVG(is, entry_name);
627 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
628 return svg == null ? null : new ImageResource(svg);
629 case OTHER:
630 while(size > 0)
631 {
632 int l = is.read(buf, offs, size);
633 offs += l;
634 size -= l;
635 }
636 BufferedImage img = null;
637 try {
638 img = ImageIO.read(new ByteArrayInputStream(buf));
639 } catch (IOException e) {
640 Main.warn(e);
641 }
642 return img == null ? null : new ImageResource(img);
643 default:
644 throw new AssertionError();
645 }
646 } finally {
647 Utils.close(is);
648 }
649 }
650 } catch (Exception e) {
651 Main.warn(tr("Failed to handle zip file ''{0}''. Exception was: {1}", archive.getName(), e.toString()));
652 } finally {
653 Utils.close(zipFile);
654 }
655 return null;
656 }
657
658 private static ImageResource getIfAvailableLocalURL(URL path, ImageType type) {
659 switch (type) {
660 case SVG:
661 URI uri = getSvgUniverse().loadSVG(path);
662 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
663 return svg == null ? null : new ImageResource(svg);
664 case OTHER:
665 BufferedImage img = null;
666 try {
667 img = ImageIO.read(path);
668 } catch (IOException e) {
669 Main.warn(e);
670 }
671 return img == null ? null : new ImageResource(img);
672 default:
673 throw new AssertionError();
674 }
675 }
676
677 private static URL getImageUrl(String path, String name, Collection<ClassLoader> additionalClassLoaders) {
678 if (path != null && path.startsWith("resource://")) {
679 String p = path.substring("resource://".length());
680 Collection<ClassLoader> classLoaders = new ArrayList<ClassLoader>(PluginHandler.getResourceClassLoaders());
681 if (additionalClassLoaders != null) {
682 classLoaders.addAll(additionalClassLoaders);
683 }
684 for (ClassLoader source : classLoaders) {
685 URL res;
686 if ((res = source.getResource(p + name)) != null)
687 return res;
688 }
689 } else {
690 File f = new File(path, name);
691 if ((path != null || f.isAbsolute()) && f.exists())
692 return Utils.fileToURL(f);
693 }
694 return null;
695 }
696
697 private static URL getImageUrl(String imageName, Collection<String> dirs, Collection<ClassLoader> additionalClassLoaders) {
698 URL u = null;
699
700 // Try passed directories first
701 if (dirs != null) {
702 for (String name : dirs) {
703 try {
704 u = getImageUrl(name, imageName, additionalClassLoaders);
705 if (u != null)
706 return u;
707 } catch (SecurityException e) {
708 Main.warn(tr(
709 "Failed to access directory ''{0}'' for security reasons. Exception was: {1}",
710 name, e.toString()));
711 }
712
713 }
714 }
715 // Try user-preference directory
716 String dir = Main.pref.getPreferencesDir() + "images";
717 try {
718 u = getImageUrl(dir, imageName, additionalClassLoaders);
719 if (u != null)
720 return u;
721 } catch (SecurityException e) {
722 Main.warn(tr(
723 "Failed to access directory ''{0}'' for security reasons. Exception was: {1}", dir, e
724 .toString()));
725 }
726
727 // Absolute path?
728 u = getImageUrl(null, imageName, additionalClassLoaders);
729 if (u != null)
730 return u;
731
732 // Try plugins and josm classloader
733 u = getImageUrl("resource://images/", imageName, additionalClassLoaders);
734 if (u != null)
735 return u;
736
737 // Try all other resource directories
738 for (String location : Main.pref.getAllPossiblePreferenceDirs()) {
739 u = getImageUrl(location + "images", imageName, additionalClassLoaders);
740 if (u != null)
741 return u;
742 u = getImageUrl(location, imageName, additionalClassLoaders);
743 if (u != null)
744 return u;
745 }
746
747 return null;
748 }
749
750 /**
751 * Reads the wiki page on a certain file in html format in order to find the real image URL.
752 */
753 private static String getImgUrlFromWikiInfoPage(final String base, final String fn) {
754
755 /** Quit parsing, when a certain condition is met */
756 class SAXReturnException extends SAXException {
757 private String result;
758
759 public SAXReturnException(String result) {
760 this.result = result;
761 }
762
763 public String getResult() {
764 return result;
765 }
766 }
767
768 try {
769 final XMLReader parser = XMLReaderFactory.createXMLReader();
770 parser.setContentHandler(new DefaultHandler() {
771 @Override
772 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
773 if (localName.equalsIgnoreCase("img")) {
774 String val = atts.getValue("src");
775 if (val.endsWith(fn))
776 throw new SAXReturnException(val); // parsing done, quit early
777 }
778 }
779 });
780
781 parser.setEntityResolver(new EntityResolver() {
782 @Override
783 public InputSource resolveEntity (String publicId, String systemId) {
784 return new InputSource(new ByteArrayInputStream(new byte[0]));
785 }
786 });
787
788 parser.parse(new InputSource(new MirroredInputStream(
789 base + fn,
790 new File(Main.pref.getPreferencesDir(), "images").toString()
791 )));
792 } catch (SAXReturnException r) {
793 return r.getResult();
794 } catch (Exception e) {
795 Main.warn("Parsing " + base + fn + " failed:\n" + e);
796 return null;
797 }
798 Main.warn("Parsing " + base + fn + " failed: Unexpected content.");
799 return null;
800 }
801
802 public static Cursor getCursor(String name, String overlay) {
803 ImageIcon img = get("cursor", name);
804 if (overlay != null) {
805 img = overlay(img, ImageProvider.get("cursor/modifier/" + overlay), OverlayPosition.SOUTHEAST);
806 }
807 Cursor c = Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(),
808 name.equals("crosshair") ? new Point(10, 10) : new Point(3, 2), "Cursor");
809 return c;
810 }
811
812 /**
813 * Decorate one icon with an overlay icon.
814 *
815 * @param ground the base image
816 * @param overlay the overlay image (can be smaller than the base image)
817 * @param pos position of the overlay image inside the base image (positioned
818 * in one of the corners)
819 * @return an icon that represent the overlay of the two given icons. The second icon is layed
820 * on the first relative to the given position.
821 */
822 public static ImageIcon overlay(Icon ground, Icon overlay, OverlayPosition pos) {
823 GraphicsConfiguration conf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()
824 .getDefaultConfiguration();
825 int w = ground.getIconWidth();
826 int h = ground.getIconHeight();
827 int wo = overlay.getIconWidth();
828 int ho = overlay.getIconHeight();
829 BufferedImage img = conf.createCompatibleImage(w, h, Transparency.TRANSLUCENT);
830 Graphics g = img.createGraphics();
831 ground.paintIcon(null, g, 0, 0);
832 int x = 0, y = 0;
833 switch (pos) {
834 case NORTHWEST:
835 x = 0;
836 y = 0;
837 break;
838 case NORTHEAST:
839 x = w - wo;
840 y = 0;
841 break;
842 case SOUTHWEST:
843 x = 0;
844 y = h - ho;
845 break;
846 case SOUTHEAST:
847 x = w - wo;
848 y = h - ho;
849 break;
850 }
851 overlay.paintIcon(null, g, x, y);
852 return new ImageIcon(img);
853 }
854
855 /** 90 degrees in radians units */
856 static final double DEGREE_90 = 90.0 * Math.PI / 180.0;
857
858 /**
859 * Creates a rotated version of the input image.
860 *
861 * @param img the image to be rotated.
862 * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
863 * will mod it with 360 before using it. More over for caching performance, it will be rounded to
864 * an entire value between 0 and 360.
865 *
866 * @return the image after rotating.
867 * @since 6172
868 */
869 public static Image createRotatedImage(Image img, double rotatedAngle) {
870 return createRotatedImage(img, rotatedAngle, ImageResource.DEFAULT_DIMENSION);
871 }
872
873 /**
874 * Creates a rotated version of the input image, scaled to the given dimension.
875 *
876 * @param img the image to be rotated.
877 * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
878 * will mod it with 360 before using it. More over for caching performance, it will be rounded to
879 * an entire value between 0 and 360.
880 * @param dimension The requested dimensions. Use (-1,-1) for the original size
881 * and (width, -1) to set the width, but otherwise scale the image proportionally.
882 * @return the image after rotating and scaling.
883 * @since 6172
884 */
885 public static Image createRotatedImage(Image img, double rotatedAngle, Dimension dimension) {
886 CheckParameterUtil.ensureParameterNotNull(img, "img");
887
888 // convert rotatedAngle to an integer value from 0 to 360
889 Long originalAngle = Math.round(rotatedAngle % 360);
890 if (rotatedAngle != 0 && originalAngle == 0) {
891 originalAngle = 360L;
892 }
893
894 ImageResource imageResource = null;
895
896 synchronized (ROTATE_CACHE) {
897 Map<Long, ImageResource> cacheByAngle = ROTATE_CACHE.get(img);
898 if (cacheByAngle == null) {
899 ROTATE_CACHE.put(img, cacheByAngle = new HashMap<Long, ImageResource>());
900 }
901
902 imageResource = cacheByAngle.get(originalAngle);
903
904 if (imageResource == null) {
905 // convert originalAngle to a value from 0 to 90
906 double angle = originalAngle % 90;
907 if (originalAngle != 0.0 && angle == 0.0) {
908 angle = 90.0;
909 }
910
911 double radian = Math.toRadians(angle);
912
913 new ImageIcon(img); // load completely
914 int iw = img.getWidth(null);
915 int ih = img.getHeight(null);
916 int w;
917 int h;
918
919 if ((originalAngle >= 0 && originalAngle <= 90) || (originalAngle > 180 && originalAngle <= 270)) {
920 w = (int) (iw * Math.sin(DEGREE_90 - radian) + ih * Math.sin(radian));
921 h = (int) (iw * Math.sin(radian) + ih * Math.sin(DEGREE_90 - radian));
922 } else {
923 w = (int) (ih * Math.sin(DEGREE_90 - radian) + iw * Math.sin(radian));
924 h = (int) (ih * Math.sin(radian) + iw * Math.sin(DEGREE_90 - radian));
925 }
926 Image image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
927 cacheByAngle.put(originalAngle, imageResource = new ImageResource(image));
928 Graphics g = image.getGraphics();
929 Graphics2D g2d = (Graphics2D) g.create();
930
931 // calculate the center of the icon.
932 int cx = iw / 2;
933 int cy = ih / 2;
934
935 // move the graphics center point to the center of the icon.
936 g2d.translate(w / 2, h / 2);
937
938 // rotate the graphics about the center point of the icon
939 g2d.rotate(Math.toRadians(originalAngle));
940
941 g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
942 g2d.drawImage(img, -cx, -cy, null);
943
944 g2d.dispose();
945 new ImageIcon(image); // load completely
946 }
947 return imageResource.getImageIcon(dimension).getImage();
948 }
949 }
950
951 /**
952 * Creates a scaled down version of the input image to fit maximum dimensions. (Keeps aspect ratio)
953 *
954 * @param img the image to be scaled down.
955 * @param maxSize the maximum size in pixels (both for width and height)
956 *
957 * @return the image after scaling.
958 * @since 6172
959 */
960 public static Image createBoundedImage(Image img, int maxSize) {
961 return new ImageResource(img).getImageIconBounded(new Dimension(maxSize, maxSize)).getImage();
962 }
963
964 /**
965 * Replies the icon for an OSM primitive type
966 * @param type the type
967 * @return the icon
968 */
969 public static ImageIcon get(OsmPrimitiveType type) {
970 CheckParameterUtil.ensureParameterNotNull(type, "type");
971 return get("data", type.getAPIName());
972 }
973
974 public static BufferedImage createImageFromSvg(SVGDiagram svg, Dimension dim) {
975 float realWidth = svg.getWidth();
976 float realHeight = svg.getHeight();
977 int width = Math.round(realWidth);
978 int height = Math.round(realHeight);
979 Double scaleX = null, scaleY = null;
980 if (dim.width != -1) {
981 width = dim.width;
982 scaleX = (double) width / realWidth;
983 if (dim.height == -1) {
984 scaleY = scaleX;
985 height = (int) Math.round(realHeight * scaleY);
986 } else {
987 height = dim.height;
988 scaleY = (double) height / realHeight;
989 }
990 } else if (dim.height != -1) {
991 height = dim.height;
992 scaleX = scaleY = (double) height / realHeight;
993 width = (int) Math.round(realWidth * scaleX);
994 }
995 if (width == 0 || height == 0) {
996 return null;
997 }
998 BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
999 Graphics2D g = img.createGraphics();
1000 g.setClip(0, 0, width, height);
1001 if (scaleX != null && scaleY != null) {
1002 g.scale(scaleX, scaleY);
1003 }
1004 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1005 try {
1006 svg.render(g);
1007 } catch (SVGException ex) {
1008 return null;
1009 }
1010 return img;
1011 }
1012
1013 private static SVGUniverse getSvgUniverse() {
1014 if (svgUniverse == null) {
1015 svgUniverse = new SVGUniverse();
1016 }
1017 return svgUniverse;
1018 }
1019}
Note: See TracBrowser for help on using the repository browser.