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                imageField.set(this, imgIcon != null ? imgIcon.getImage() : null);
136            } else {
137                Method loadImage = ImageView.class.getDeclaredMethod("loadImage");
138                ReflectionUtils.setObjectsAccessible(loadImage);
139                loadImage.invoke(this);
140            }
141        } else {
142            imageField.set(this, null);
143        }
144    }
145
146    @Override
147    public Image getImage() {
148        doSync();
149        return super.getImage();
150    }
151
152    @Override
153    public AttributeSet getAttributes() {
154        doSync();
155        return super.getAttributes();
156    }
157
158    @Override
159    public void paint(Graphics g, Shape a) {
160        doSync();
161        super.paint(g, a);
162    }
163
164    @Override
165    public float getPreferredSpan(int axis) {
166        doSync();
167        return super.getPreferredSpan(axis);
168    }
169
170    @Override
171    public void setSize(float width, float height) {
172        doSync();
173        super.setSize(width, height);
174    }
175}