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}