001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.widgets; 003 004import java.awt.Graphics; 005import java.awt.Image; 006import java.awt.Shape; 007import java.lang.reflect.Field; 008import java.lang.reflect.InvocationTargetException; 009import java.lang.reflect.Method; 010import java.net.URL; 011 012import javax.swing.ImageIcon; 013import javax.swing.text.AttributeSet; 014import javax.swing.text.Element; 015import javax.swing.text.html.ImageView; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.tools.ImageProvider; 019 020/** 021 * Specialized Image View allowing to display SVG images. 022 * @since 8933 023 */ 024public class JosmImageView extends ImageView { 025 026 private static final int LOADING_FLAG = 1; 027 private static final int WIDTH_FLAG = 4; 028 private static final int HEIGHT_FLAG = 8; 029 private static final int RELOAD_FLAG = 16; 030 private static final int RELOAD_IMAGE_FLAG = 32; 031 032 private final Field imageField; 033 private final Field stateField; 034 private final Field widthField; 035 private final Field heightField; 036 037 /** 038 * Constructs a new {@code JosmImageView}. 039 * @param elem the element to create a view for 040 * @throws SecurityException see {@link Class#getDeclaredField} for details 041 * @throws NoSuchFieldException see {@link Class#getDeclaredField} for details 042 */ 043 public JosmImageView(Element elem) throws NoSuchFieldException, SecurityException { 044 super(elem); 045 imageField = ImageView.class.getDeclaredField("image"); 046 stateField = ImageView.class.getDeclaredField("state"); 047 widthField = ImageView.class.getDeclaredField("width"); 048 heightField = ImageView.class.getDeclaredField("height"); 049 imageField.setAccessible(true); 050 stateField.setAccessible(true); 051 widthField.setAccessible(true); 052 heightField.setAccessible(true); 053 } 054 055 /** 056 * Makes sure the necessary properties and image is loaded. 057 */ 058 private void sync() { 059 try { 060 int s = (int) stateField.get(this); 061 if ((s & RELOAD_IMAGE_FLAG) != 0) { 062 refreshImage(); 063 } 064 s = (int) stateField.get(this); 065 if ((s & RELOAD_FLAG) != 0) { 066 synchronized (this) { 067 stateField.set(this, ((int) stateField.get(this) | RELOAD_FLAG) ^ RELOAD_FLAG); 068 } 069 setPropertiesFromAttributes(); 070 } 071 } catch (IllegalArgumentException | IllegalAccessException | 072 InvocationTargetException | NoSuchMethodException | SecurityException e) { 073 Main.error(e); 074 } 075 } 076 077 /** 078 * Loads the image and updates the size accordingly. This should be 079 * invoked instead of invoking <code>loadImage</code> or 080 * <code>updateImageSize</code> directly. 081 * @throws IllegalAccessException see {@link Field#set} and {@link Method#invoke} for details 082 * @throws IllegalArgumentException see {@link Field#set} and {@link Method#invoke} for details 083 * @throws InvocationTargetException see {@link Method#invoke} for details 084 * @throws NoSuchMethodException see {@link Class#getDeclaredMethod} for details 085 * @throws SecurityException see {@link Class#getDeclaredMethod} for details 086 */ 087 private void refreshImage() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, 088 NoSuchMethodException, SecurityException { 089 synchronized (this) { 090 // clear out width/height/reloadimage flag and set loading flag 091 stateField.set(this, ((int) stateField.get(this) | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG | 092 HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG | 093 RELOAD_IMAGE_FLAG)); 094 imageField.set(this, null); 095 widthField.set(this, 0); 096 heightField.set(this, 0); 097 } 098 099 try { 100 // Load the image 101 loadImage(); 102 103 // And update the size params 104 Method updateImageSize = ImageView.class.getDeclaredMethod("updateImageSize"); 105 updateImageSize.setAccessible(true); 106 updateImageSize.invoke(this); 107 } finally { 108 synchronized (this) { 109 // Clear out state in case someone threw an exception. 110 stateField.set(this, ((int) stateField.get(this) | LOADING_FLAG) ^ LOADING_FLAG); 111 } 112 } 113 } 114 115 /** 116 * Loads the image from the URL <code>getImageURL</code>. This should 117 * only be invoked from <code>refreshImage</code>. 118 * @throws IllegalAccessException see {@link Field#set} and {@link Method#invoke} for details 119 * @throws IllegalArgumentException see {@link Field#set} and {@link Method#invoke} for details 120 * @throws InvocationTargetException see {@link Method#invoke} for details 121 * @throws NoSuchMethodException see {@link Class#getDeclaredMethod} for details 122 * @throws SecurityException see {@link Class#getDeclaredMethod} for details 123 */ 124 private void loadImage() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, 125 NoSuchMethodException, SecurityException { 126 URL src = getImageURL(); 127 if (src != null) { 128 String urlStr = src.toExternalForm(); 129 if (urlStr.endsWith(".svg") || urlStr.endsWith(".svg?format=raw")) { 130 ImageIcon imgIcon = new ImageProvider(urlStr).setOptional(true).get(); 131 imageField.set(this, imgIcon != null ? imgIcon.getImage() : null); 132 } else { 133 Method loadImage = ImageView.class.getDeclaredMethod("loadImage"); 134 loadImage.setAccessible(true); 135 loadImage.invoke(this); 136 } 137 } else { 138 imageField.set(this, null); 139 } 140 } 141 142 @Override 143 public Image getImage() { 144 sync(); 145 return super.getImage(); 146 } 147 148 @Override 149 public AttributeSet getAttributes() { 150 sync(); 151 return super.getAttributes(); 152 } 153 154 @Override 155 public void paint(Graphics g, Shape a) { 156 sync(); 157 super.paint(g, a); 158 } 159 160 @Override 161 public float getPreferredSpan(int axis) { 162 sync(); 163 return super.getPreferredSpan(axis); 164 } 165 166 @Override 167 public void setSize(float width, float height) { 168 sync(); 169 super.setSize(width, height); 170 } 171}