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