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

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

code refactoring to avoid classes in methods (cause problems for code analysis tools)

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