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.Utils;
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 = ImageView.class.getDeclaredField("image");
047        stateField = ImageView.class.getDeclaredField("state");
048        widthField = ImageView.class.getDeclaredField("width");
049        heightField = ImageView.class.getDeclaredField("height");
050        Utils.setObjectsAccessible(imageField, stateField, widthField, heightField);
051    }
052
053    /**
054     * Makes sure the necessary properties and image is loaded.
055     */
056    private void doSync() {
057        try {
058            int s = (int) stateField.get(this);
059            if ((s & RELOAD_IMAGE_FLAG) != 0) {
060                doRefreshImage();
061            }
062            s = (int) stateField.get(this);
063            if ((s & RELOAD_FLAG) != 0) {
064                synchronized (this) {
065                    stateField.set(this, ((int) stateField.get(this) | RELOAD_FLAG) ^ RELOAD_FLAG);
066                }
067                setPropertiesFromAttributes();
068            }
069        } catch (IllegalArgumentException | ReflectiveOperationException | SecurityException e) {
070           Logging.error(e);
071       }
072    }
073
074    /**
075     * Loads the image and updates the size accordingly. This should be
076     * invoked instead of invoking <code>loadImage</code> or
077     * <code>updateImageSize</code> directly.
078     * @throws IllegalAccessException see {@link Field#set} and {@link Method#invoke} for details
079     * @throws IllegalArgumentException see {@link Field#set} and {@link Method#invoke} for details
080     * @throws InvocationTargetException see {@link Method#invoke} for details
081     * @throws NoSuchMethodException see {@link Class#getDeclaredMethod} for details
082     * @throws SecurityException see {@link Class#getDeclaredMethod} for details
083     */
084    private void doRefreshImage() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
085        synchronized (this) {
086            // clear out width/height/reloadimage flag and set loading flag
087            stateField.set(this, ((int) stateField.get(this) | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG |
088                     HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG |
089                                     RELOAD_IMAGE_FLAG));
090            imageField.set(this, null);
091            widthField.set(this, 0);
092            heightField.set(this, 0);
093        }
094
095        try {
096            // Load the image
097            doLoadImage();
098
099            // And update the size params
100            Method updateImageSize = ImageView.class.getDeclaredMethod("updateImageSize");
101            Utils.setObjectsAccessible(updateImageSize);
102            updateImageSize.invoke(this);
103        } finally {
104            synchronized (this) {
105                // Clear out state in case someone threw an exception.
106                stateField.set(this, ((int) stateField.get(this) | LOADING_FLAG) ^ LOADING_FLAG);
107            }
108        }
109    }
110
111    /**
112     * Loads the image from the URL <code>getImageURL</code>. This should
113     * only be invoked from <code>refreshImage</code>.
114     * @throws IllegalAccessException see {@link Field#set} and {@link Method#invoke} for details
115     * @throws IllegalArgumentException see {@link Field#set} and {@link Method#invoke} for details
116     * @throws InvocationTargetException see {@link Method#invoke} for details
117     * @throws NoSuchMethodException see {@link Class#getDeclaredMethod} for details
118     * @throws SecurityException see {@link Class#getDeclaredMethod} for details
119     */
120    private void doLoadImage() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
121        URL src = getImageURL();
122        if (src != null) {
123            String urlStr = src.toExternalForm();
124            if (urlStr.endsWith(".svg") || urlStr.endsWith(".svg?format=raw")) {
125                ImageIcon imgIcon = new ImageProvider(urlStr).setOptional(true).get();
126                imageField.set(this, imgIcon != null ? imgIcon.getImage() : null);
127            } else {
128                Method loadImage = ImageView.class.getDeclaredMethod("loadImage");
129                Utils.setObjectsAccessible(loadImage);
130                loadImage.invoke(this);
131            }
132        } else {
133            imageField.set(this, null);
134        }
135    }
136
137    @Override
138    public Image getImage() {
139        doSync();
140        return super.getImage();
141    }
142
143    @Override
144    public AttributeSet getAttributes() {
145        doSync();
146        return super.getAttributes();
147    }
148
149    @Override
150    public void paint(Graphics g, Shape a) {
151        doSync();
152        super.paint(g, a);
153    }
154
155    @Override
156    public float getPreferredSpan(int axis) {
157        doSync();
158        return super.getPreferredSpan(axis);
159    }
160
161    @Override
162    public void setSize(float width, float height) {
163        doSync();
164        super.setSize(width, height);
165    }
166}