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