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

Last change on this file since 4403 was 4403, checked in by bastiK, 13 years ago

applied #6137 - provide osm wiki image retrieval in ImageProvider (based on patch by cmuelle8)

  • Property svn:eol-style set to native
File size: 24.1 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Component;
7import java.awt.Cursor;
8import java.awt.Dimension;
9import java.awt.Graphics;
10import java.awt.Graphics2D;
11import java.awt.GraphicsConfiguration;
12import java.awt.GraphicsEnvironment;
13import java.awt.Image;
14import java.awt.Point;
15import java.awt.RenderingHints;
16import java.awt.Toolkit;
17import java.awt.Transparency;
18import java.awt.image.BufferedImage;
19import java.io.ByteArrayInputStream;
20import java.io.File;
21import java.io.IOException;
22import java.io.InputStream;
23import java.net.MalformedURLException;
24import java.net.URI;
25import java.net.URL;
26import java.util.Arrays;
27import java.util.Collection;
28import java.util.HashMap;
29import java.util.Map;
30import java.util.zip.ZipEntry;
31import java.util.zip.ZipFile;
32import javax.swing.Icon;
33import javax.swing.ImageIcon;
34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
37import org.openstreetmap.josm.io.MirroredInputStream;
38import org.openstreetmap.josm.plugins.PluginHandler;
39import org.openstreetmap.josm.tools.Utils;
40import org.xml.sax.Attributes;
41import org.xml.sax.EntityResolver;
42import org.xml.sax.InputSource;
43import org.xml.sax.SAXException;
44import org.xml.sax.XMLReader;
45import org.xml.sax.helpers.DefaultHandler;
46import org.xml.sax.helpers.XMLReaderFactory;
47
48import com.kitfox.svg.SVGDiagram;
49import com.kitfox.svg.SVGException;
50import com.kitfox.svg.SVGUniverse;
51
52/**
53 * Helper class to support the application with images.
54 * @author imi
55 */
56public class ImageProvider {
57
58 private static SVGUniverse svgUniverse;
59
60 /**
61 * Position of an overlay icon
62 * @author imi
63 */
64 public static enum OverlayPosition {
65 NORTHWEST, NORTHEAST, SOUTHWEST, SOUTHEAST
66 }
67
68 public static enum ImageType {
69 SVG, // scalable vector graphics
70 OTHER // everything else, e.g. png, gif (must be supported by Java)
71 }
72
73 /**
74 * The icon cache
75 */
76 private static Map<String, ImageResource> cache = new HashMap<String, ImageResource>();
77
78 /**
79 * Return an image from the specified location.
80 *
81 * @param subdir The position of the directory, e.g. 'layer'
82 * @param name The icons name (with or without '.png' or '.svg' extension)
83 * @return The requested Image.
84 */
85 public static ImageIcon get(String subdir, String name) {
86 ImageIcon icon = getIfAvailable(subdir, name);
87 if (icon == null) {
88 String ext = name.indexOf('.') != -1 ? "" : ".???";
89 throw new NullPointerException(tr(
90 "Fatal: failed to locate image ''{0}''. This is a serious configuration problem. JOSM will stop working.",
91 name+ext));
92 }
93 return icon;
94 }
95
96 /**
97 * Shortcut for get("", name);
98 */
99 public static ImageIcon get(String name) {
100 return get("", name);
101 }
102
103 public static ImageIcon getIfAvailable(String subdir, String name) {
104 return getIfAvailable((Collection<String>) null, null, subdir, name);
105 }
106
107 public static ImageIcon getIfAvailable(String[] dirs, String id, String subdir, String name) {
108 return getIfAvailable(Arrays.asList(dirs), id, subdir, name);
109 }
110
111 /**
112 * Like {@link #get(String)}, but does not throw and return <code>null</code> in case of nothing
113 * is found. Use this, if the image to retrieve is optional. Nevertheless a warning will
114 * be printed on the console if the image could not be found.
115 */
116 public static ImageIcon getIfAvailable(Collection<String> dirs, String id, String subdir, String name) {
117 return getIfAvailable(dirs, id, subdir, name, null);
118 }
119
120 public static ImageIcon getIfAvailable(Collection<String> dirs, String id, String subdir, String name, File archive) {
121 return getIfAvailable(dirs, id, subdir, name, archive, false);
122 }
123
124 public static ImageIcon getIfAvailable(Collection<String> dirs, String id, String subdir, String name, File archive, boolean sanitize) {
125 return getIfAvailable(dirs, id, subdir, name, archive, null, sanitize);
126 }
127
128 /**
129 * The full path of the image is either a url (starting with http://)
130 * or something like
131 * dirs.get(i)+"/"+subdir+"/"+name+".png".
132 * @param dirs Directories to look (may be null).
133 * @param id An id used for caching. Id is not used for cache if name starts with http://. (URL is unique anyway.)
134 * @param subdir Subdirectory the image lies in.
135 * @param name The name of the image. If it does not end with '.png' or '.svg',
136 * it will try both extensions.
137 * @param archive A zip file where the image is located (may be null).
138 * @param dim The dimensions of the image if it should be scaled. null if the
139 * original size of the image should be returned. The width
140 * part of the dimension can be -1. Then it will scale the width
141 * in the same way as the height. (And the other way around.)
142 * @param sanitize If the image should be repainted to a new BufferedImage to work
143 * around certain issues.
144 */
145 public static ImageIcon getIfAvailable(Collection<String> dirs, String id, String subdir, String name, File archive, Dimension dim, boolean sanitize) {
146 ImageResource ir = getIfAvailableImpl(dirs, id, subdir, name, archive);
147 if (ir == null)
148 return null;
149 return ir.getImageIcon(dim == null ? ImageResource.DEFAULT_DIMENSION : dim, sanitize);
150 }
151
152 private static ImageResource getIfAvailableImpl(Collection<String> dirs, String id, String subdir, String name, File archive) {
153 if (name == null)
154 return null;
155 ImageType type = name.toLowerCase().endsWith(".svg") ? ImageType.SVG : ImageType.OTHER;
156
157 if (name.startsWith("http://")) {
158 String url = name;
159 ImageResource ir = cache.get(url);
160 if (ir != null) return ir;
161 ir = getIfAvailableHttp(url, type);
162 if (ir != null) {
163 cache.put(url, ir);
164 }
165 return ir;
166 } else if (name.startsWith("wiki://")) {
167 ImageResource ir = cache.get(name);
168 if (ir != null) return ir;
169 ir = getIfAvailableWiki(name, type);
170 if (ir != null) {
171 cache.put(name, ir);
172 }
173 return ir;
174 }
175
176 if (subdir == null) {
177 subdir = "";
178 } else if (!subdir.equals("")) {
179 subdir += "/";
180 }
181 String[] extensions;
182 if (name.indexOf('.') != -1) {
183 extensions = new String[] { "" };
184 } else {
185 extensions = new String[] { ".png", ".svg"};
186 }
187 final int ARCHIVE = 0, LOCAL = 1;
188 for (int place : new Integer[] { ARCHIVE, LOCAL }) {
189 for (String ext : extensions) {
190
191 if (".svg".equals(ext)) {
192 type = ImageType.SVG;
193 } else if (".png".equals(ext)) {
194 type = ImageType.OTHER;
195 }
196
197 String full_name = subdir + name + ext;
198 String cache_name = full_name;
199 /* cache separately */
200 if (dirs != null && dirs.size() > 0) {
201 cache_name = "id:" + id + ":" + full_name;
202 if(archive != null) {
203 cache_name += ":" + archive.getName();
204 }
205 }
206
207 ImageResource ir = cache.get(cache_name);
208 if (ir != null) return ir;
209
210 switch (place) {
211 case ARCHIVE:
212 if (archive != null) {
213 ir = getIfAvailableZip(full_name, archive, type);
214 if (ir != null) {
215 cache.put(cache_name, ir);
216 return ir;
217 }
218 }
219 break;
220 case LOCAL:
221 // getImageUrl() does a ton of "stat()" calls and gets expensive
222 // and redundant when you have a whole ton of objects. So,
223 // index the cache by the name of the icon we're looking for
224 // and don't bother to create a URL unless we're actually
225 // creating the image.
226 URL path = getImageUrl(full_name, dirs);
227 if (path == null) {
228 continue;
229 }
230 ir = getIfAvailableLocalURL(path, type);
231 if (ir != null) {
232 cache.put(cache_name, ir);
233 return ir;
234 }
235 break;
236 }
237 }
238 }
239 return null;
240 }
241
242 private static ImageResource getIfAvailableHttp(String url, ImageType type) {
243 try {
244 MirroredInputStream is = new MirroredInputStream(url,
245 new File(Main.pref.getPreferencesDir(), "images").toString());
246 switch (type) {
247 case SVG:
248 URI uri = getSvgUniverse().loadSVG(is, is.getFile().toURI().toURL().toString());
249 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
250 return svg == null ? null : new ImageResource(svg);
251 case OTHER:
252 Image img = Toolkit.getDefaultToolkit().createImage(is.getFile().toURI().toURL());
253 return img == null ? null : new ImageResource(img, false);
254 default:
255 throw new AssertionError();
256 }
257 } catch (IOException e) {
258 return null;
259 }
260 }
261
262 private static ImageResource getIfAvailableWiki(String name, ImageType type) {
263 final Collection<String> defaultBaseUrls = Arrays.asList(
264 "http://wiki.openstreetmap.org/w/images/",
265 "http://upload.wikimedia.org/wikipedia/commons/",
266 "http://wiki.openstreetmap.org/wiki/File:"
267 );
268 final Collection<String> baseUrls = Main.pref.getCollection("image-provider.wiki.urls", defaultBaseUrls);
269
270 final String fn = name.substring(name.lastIndexOf('/') + 1);
271
272 ImageResource result = null;
273 for (String b : baseUrls) {
274 String url;
275 if (b.endsWith(":")) {
276 url = getImgUrlFromWikiInfoPage(b, fn);
277 if (url == null) {
278 continue;
279 }
280 } else {
281 final String fn_md5 = Utils.md5Hex(fn);
282 url = b + fn_md5.substring(0,1) + "/" + fn_md5.substring(0,2) + "/" + fn;
283 }
284 result = getIfAvailableHttp(url, type);
285 if (result != null) {
286 break;
287 }
288 }
289 return result;
290 }
291
292 private static ImageResource getIfAvailableZip(String full_name, File archive, ImageType type) {
293 ZipFile zipFile = null;
294 try
295 {
296 zipFile = new ZipFile(archive);
297 ZipEntry entry = zipFile.getEntry(full_name);
298 if(entry != null)
299 {
300 int size = (int)entry.getSize();
301 int offs = 0;
302 byte[] buf = new byte[size];
303 InputStream is = null;
304 try {
305 is = zipFile.getInputStream(entry);
306 switch (type) {
307 case SVG:
308 URI uri = getSvgUniverse().loadSVG(is, full_name);
309 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
310 return svg == null ? null : new ImageResource(svg);
311 case OTHER:
312 while(size > 0)
313 {
314 int l = is.read(buf, offs, size);
315 offs += l;
316 size -= l;
317 }
318 Image img = Toolkit.getDefaultToolkit().createImage(buf);
319 return img == null ? null : new ImageResource(img, false);
320 default:
321 throw new AssertionError();
322 }
323 } finally {
324 if (is != null) {
325 is.close();
326 }
327 }
328 }
329 } catch (Exception e) {
330 System.err.println(tr("Warning: failed to handle zip file ''{0}''. Exception was: {1}", archive.getName(), e.toString()));
331 } finally {
332 if (zipFile != null) {
333 try {
334 zipFile.close();
335 } catch (IOException ex) {
336 }
337 }
338 }
339 return null;
340 }
341
342 private static ImageResource getIfAvailableLocalURL(URL path, ImageType type) {
343 switch (type) {
344 case SVG:
345 URI uri = getSvgUniverse().loadSVG(path);
346 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
347 return svg == null ? null : new ImageResource(svg);
348 case OTHER:
349 Image img = Toolkit.getDefaultToolkit().createImage(path);
350 return img == null ? null : new ImageResource(img, false);
351 default:
352 throw new AssertionError();
353 }
354 }
355
356 private static URL getImageUrl(String path, String name) {
357 if (path != null && path.startsWith("resource://")) {
358 String p = path.substring("resource://".length());
359 for (ClassLoader source : PluginHandler.getResourceClassLoaders()) {
360 URL res;
361 if ((res = source.getResource(p + name)) != null)
362 return res;
363 }
364 } else {
365 try {
366 File f = new File(path, name);
367 if (f.exists())
368 return f.toURI().toURL();
369 } catch (MalformedURLException e) {
370 }
371 }
372 return null;
373 }
374
375 private static URL getImageUrl(String imageName, Collection<String> dirs) {
376 URL u = null;
377
378 // Try passed directories first
379 if (dirs != null) {
380 for (String name : dirs) {
381 try {
382 u = getImageUrl(name, imageName);
383 if (u != null)
384 return u;
385 } catch (SecurityException e) {
386 System.out.println(tr(
387 "Warning: failed to access directory ''{0}'' for security reasons. Exception was: {1}",
388 name, e.toString()));
389 }
390
391 }
392 }
393 // Try user-preference directory
394 String dir = Main.pref.getPreferencesDir() + "images";
395 try {
396 u = getImageUrl(dir, imageName);
397 if (u != null)
398 return u;
399 } catch (SecurityException e) {
400 System.out.println(tr(
401 "Warning: failed to access directory ''{0}'' for security reasons. Exception was: {1}", dir, e
402 .toString()));
403 }
404
405 // Absolute path?
406 u = getImageUrl(null, imageName);
407 if (u != null)
408 return u;
409
410 // Try plugins and josm classloader
411 u = getImageUrl("resource://images/", imageName);
412 if (u != null)
413 return u;
414
415 // Try all other resource directories
416 for (String location : Main.pref.getAllPossiblePreferenceDirs()) {
417 u = getImageUrl(location + "images", imageName);
418 if (u != null)
419 return u;
420 u = getImageUrl(location, imageName);
421 if (u != null)
422 return u;
423 }
424
425 return null;
426 }
427
428 /**
429 * Reads the wiki page on a certain file in html format in order to find the real image URL.
430 */
431 private static String getImgUrlFromWikiInfoPage(final String base, final String fn) {
432
433 /** Quit parsing, when a certain condition is met */
434 class SAXReturnException extends SAXException {
435 private String result;
436
437 public SAXReturnException(String result) {
438 this.result = result;
439 }
440
441 public String getResult() {
442 return result;
443 }
444 }
445
446 try {
447 final XMLReader parser = XMLReaderFactory.createXMLReader();
448 parser.setContentHandler(new DefaultHandler() {
449 @Override
450 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
451 System.out.println();
452 if (localName.equalsIgnoreCase("img")) {
453 String val = atts.getValue("src");
454 if (val.endsWith(fn))
455 throw new SAXReturnException(val); // parsing done, quit early
456 }
457 }
458 });
459
460 parser.setEntityResolver(new EntityResolver() {
461 public InputSource resolveEntity (String publicId, String systemId) {
462 return new InputSource(new ByteArrayInputStream(new byte[0]));
463 }
464 });
465
466 parser.parse(new InputSource(new MirroredInputStream(
467 base + fn,
468 new File(Main.pref.getPreferencesDir(), "images").toString()
469 )));
470 } catch (SAXReturnException r) {
471 return r.getResult();
472 } catch (Exception e) {
473 System.out.println("INFO: parsing " + base + fn + " failed:\n" + e);
474 return null;
475 }
476 System.out.println("INFO: parsing " + base + fn + " failed: Unexpected content.");
477 return null;
478 }
479
480 public static Cursor getCursor(String name, String overlay) {
481 ImageIcon img = get("cursor", name);
482 if (overlay != null) {
483 img = overlay(img, "cursor/modifier/" + overlay, OverlayPosition.SOUTHEAST);
484 }
485 Cursor c = Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(),
486 name.equals("crosshair") ? new Point(10, 10) : new Point(3, 2), "Cursor");
487 return c;
488 }
489
490 /**
491 * @return an icon that represent the overlay of the two given icons. The second icon is layed
492 * on the first relative to the given position.
493 */
494 public static ImageIcon overlay(Icon ground, String overlayImage, OverlayPosition pos) {
495 return overlay(ground, ImageProvider.get(overlayImage), pos);
496 }
497
498 public static ImageIcon overlay(Icon ground, Icon overlay, OverlayPosition pos) {
499 GraphicsConfiguration conf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()
500 .getDefaultConfiguration();
501 int w = ground.getIconWidth();
502 int h = ground.getIconHeight();
503 int wo = overlay.getIconWidth();
504 int ho = overlay.getIconHeight();
505 BufferedImage img = conf.createCompatibleImage(w, h, Transparency.TRANSLUCENT);
506 Graphics g = img.createGraphics();
507 ground.paintIcon(null, g, 0, 0);
508 int x = 0, y = 0;
509 switch (pos) {
510 case NORTHWEST:
511 x = 0;
512 y = 0;
513 break;
514 case NORTHEAST:
515 x = w - wo;
516 y = 0;
517 break;
518 case SOUTHWEST:
519 x = 0;
520 y = h - ho;
521 break;
522 case SOUTHEAST:
523 x = w - wo;
524 y = h - ho;
525 break;
526 }
527 overlay.paintIcon(null, g, x, y);
528 return new ImageIcon(img);
529 }
530
531 /*
532 * from: http://www.jidesoft.com/blog/2008/02/29/rotate-an-icon-in-java/ License:
533 * "feel free to use"
534 */
535 final static double DEGREE_90 = 90.0 * Math.PI / 180.0;
536
537 /**
538 * Creates a rotated version of the input image.
539 *
540 * @param c The component to get properties useful for painting, e.g. the foreground or
541 * background color.
542 * @param icon the image to be rotated.
543 * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
544 * will mod it with 360 before using it.
545 *
546 * @return the image after rotating.
547 */
548 public static ImageIcon createRotatedImage(Component c, Icon icon, double rotatedAngle) {
549 // convert rotatedAngle to a value from 0 to 360
550 double originalAngle = rotatedAngle % 360;
551 if (rotatedAngle != 0 && originalAngle == 0) {
552 originalAngle = 360.0;
553 }
554
555 // convert originalAngle to a value from 0 to 90
556 double angle = originalAngle % 90;
557 if (originalAngle != 0.0 && angle == 0.0) {
558 angle = 90.0;
559 }
560
561 double radian = Math.toRadians(angle);
562
563 int iw = icon.getIconWidth();
564 int ih = icon.getIconHeight();
565 int w;
566 int h;
567
568 if ((originalAngle >= 0 && originalAngle <= 90) || (originalAngle > 180 && originalAngle <= 270)) {
569 w = (int) (iw * Math.sin(DEGREE_90 - radian) + ih * Math.sin(radian));
570 h = (int) (iw * Math.sin(radian) + ih * Math.sin(DEGREE_90 - radian));
571 } else {
572 w = (int) (ih * Math.sin(DEGREE_90 - radian) + iw * Math.sin(radian));
573 h = (int) (ih * Math.sin(radian) + iw * Math.sin(DEGREE_90 - radian));
574 }
575 BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
576 Graphics g = image.getGraphics();
577 Graphics2D g2d = (Graphics2D) g.create();
578
579 // calculate the center of the icon.
580 int cx = iw / 2;
581 int cy = ih / 2;
582
583 // move the graphics center point to the center of the icon.
584 g2d.translate(w / 2, h / 2);
585
586 // rotate the graphics about the center point of the icon
587 g2d.rotate(Math.toRadians(originalAngle));
588
589 g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
590 icon.paintIcon(c, g2d, -cx, -cy);
591
592 g2d.dispose();
593 return new ImageIcon(image);
594 }
595
596 /**
597 * Replies the icon for an OSM primitive type
598 * @param type the type
599 * @return the icon
600 */
601 public static ImageIcon get(OsmPrimitiveType type) throws IllegalArgumentException {
602 CheckParameterUtil.ensureParameterNotNull(type, "type");
603 return get("data", type.getAPIName());
604 }
605
606 public static BufferedImage sanitize(Image img) {
607 (new ImageIcon(img)).getImage(); // load competely
608 int width = img.getWidth(null);
609 int height = img.getHeight(null);
610 BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
611 result.getGraphics().drawImage(img, 0, 0, null);
612 return result;
613 }
614
615 public static Image createImageFromSvg(SVGDiagram svg, Dimension dim) {
616 float realWidth = svg.getWidth();
617 float realHeight = svg.getHeight();
618 int width = Math.round(realWidth);
619 int height = Math.round(realHeight);
620 Double scaleX = null, scaleY = null;
621 if (dim.width != -1) {
622 width = dim.width;
623 scaleX = (double) width / realWidth;
624 if (dim.height == -1) {
625 scaleY = scaleX;
626 height = (int) Math.round(realHeight * scaleY);
627 } else {
628 height = dim.height;
629 scaleY = (double) height / realHeight;
630 }
631 } else if (dim.height != -1) {
632 height = dim.height;
633 scaleX = scaleY = (double) height / realHeight;
634 width = (int) Math.round(realWidth * scaleX);
635 }
636 Image img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
637 Graphics2D g = ((BufferedImage) img).createGraphics();
638 g.setClip(0, 0, width, height);
639 if (scaleX != null) {
640 g.scale(scaleX, scaleY);
641 }
642 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
643 try {
644 svg.render(g);
645 } catch (SVGException ex) {
646 return null;
647 }
648 return img;
649 }
650
651 private static SVGUniverse getSvgUniverse() {
652 if (svgUniverse == null) {
653 svgUniverse = new SVGUniverse();
654 }
655 return svgUniverse;
656 }
657}
Note: See TracBrowser for help on using the repository browser.