001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trc;
007
008import java.awt.Color;
009import java.awt.Component;
010import java.awt.Font;
011import java.awt.Graphics2D;
012import java.awt.GridBagLayout;
013import java.awt.Transparency;
014import java.awt.event.ActionEvent;
015import java.awt.font.FontRenderContext;
016import java.awt.font.LineBreakMeasurer;
017import java.awt.font.TextAttribute;
018import java.awt.font.TextLayout;
019import java.awt.image.BufferedImage;
020import java.awt.image.BufferedImageOp;
021import java.awt.image.ConvolveOp;
022import java.awt.image.Kernel;
023import java.awt.image.LookupOp;
024import java.awt.image.ShortLookupTable;
025import java.text.AttributedCharacterIterator;
026import java.text.AttributedString;
027import java.util.ArrayList;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031
032import javax.swing.AbstractAction;
033import javax.swing.Icon;
034import javax.swing.JCheckBoxMenuItem;
035import javax.swing.JComponent;
036import javax.swing.JLabel;
037import javax.swing.JMenu;
038import javax.swing.JMenuItem;
039import javax.swing.JPanel;
040import javax.swing.JPopupMenu;
041import javax.swing.JSeparator;
042
043import org.openstreetmap.josm.Main;
044import org.openstreetmap.josm.actions.ImageryAdjustAction;
045import org.openstreetmap.josm.data.ProjectionBounds;
046import org.openstreetmap.josm.data.imagery.ImageryInfo;
047import org.openstreetmap.josm.data.imagery.OffsetBookmark;
048import org.openstreetmap.josm.data.preferences.ColorProperty;
049import org.openstreetmap.josm.data.preferences.IntegerProperty;
050import org.openstreetmap.josm.gui.MenuScroller;
051import org.openstreetmap.josm.gui.widgets.UrlLabel;
052import org.openstreetmap.josm.tools.GBC;
053import org.openstreetmap.josm.tools.ImageProvider;
054import org.openstreetmap.josm.tools.Utils;
055
056public abstract class ImageryLayer extends Layer {
057
058    public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white);
059    public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
060    public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
061
062    private final List<ImageProcessor> imageProcessors = new ArrayList<>();
063
064    public static Color getFadeColor() {
065        return PROP_FADE_COLOR.get();
066    }
067
068    public static Color getFadeColorWithAlpha() {
069        Color c = PROP_FADE_COLOR.get();
070        return new Color(c.getRed(), c.getGreen(), c.getBlue(), PROP_FADE_AMOUNT.get()*255/100);
071    }
072
073    protected final ImageryInfo info;
074
075    protected Icon icon;
076
077    protected double dx;
078    protected double dy;
079
080    protected GammaImageProcessor gammaImageProcessor = new GammaImageProcessor();
081
082    private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
083
084    /**
085     * Constructs a new {@code ImageryLayer}.
086     * @param info imagery info
087     */
088    public ImageryLayer(ImageryInfo info) {
089        super(info.getName());
090        this.info = info;
091        if (info.getIcon() != null) {
092            icon = new ImageProvider(info.getIcon()).setOptional(true).
093                    setMaxHeight(ICON_SIZE).setMaxWidth(ICON_SIZE).get();
094        }
095        if (icon == null) {
096            icon = ImageProvider.get("imagery_small");
097        }
098        addImageProcessor(createSharpener(PROP_SHARPEN_LEVEL.get()));
099        addImageProcessor(gammaImageProcessor);
100    }
101
102    public double getPPD() {
103        if (!Main.isDisplayingMapView()) return Main.getProjection().getDefaultZoomInPPD();
104        ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
105        return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast);
106    }
107
108    public double getDx() {
109        return dx;
110    }
111
112    public double getDy() {
113        return dy;
114    }
115
116    public void setOffset(double dx, double dy) {
117        this.dx = dx;
118        this.dy = dy;
119    }
120
121    public void displace(double dx, double dy) {
122        setOffset(this.dx += dx, this.dy += dy);
123    }
124
125    public ImageryInfo getInfo() {
126        return info;
127    }
128
129    @Override
130    public Icon getIcon() {
131        return icon;
132    }
133
134    @Override
135    public boolean isMergable(Layer other) {
136        return false;
137    }
138
139    @Override
140    public void mergeFrom(Layer from) {
141    }
142
143    @Override
144    public Object getInfoComponent() {
145        JPanel panel = new JPanel(new GridBagLayout());
146        panel.add(new JLabel(getToolTipText()), GBC.eol());
147        if (info != null) {
148            String url = info.getUrl();
149            if (url != null) {
150                panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
151                panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
152            }
153            if (dx != 0 || dy != 0) {
154                panel.add(new JLabel(tr("Offset: ") + dx + ';' + dy), GBC.eol().insets(0, 5, 10, 0));
155            }
156        }
157        return panel;
158    }
159
160    public static ImageryLayer create(ImageryInfo info) {
161        switch(info.getImageryType()) {
162        case WMS:
163        case HTML:
164            return new WMSLayer(info);
165        case WMTS:
166            return new WMTSLayer(info);
167        case TMS:
168        case BING:
169        case SCANEX:
170            return new TMSLayer(info);
171        default:
172            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
173        }
174    }
175
176    class ApplyOffsetAction extends AbstractAction {
177        private final transient OffsetBookmark b;
178
179        ApplyOffsetAction(OffsetBookmark b) {
180            super(b.name);
181            this.b = b;
182        }
183
184        @Override
185        public void actionPerformed(ActionEvent ev) {
186            setOffset(b.dx, b.dy);
187            Main.main.menu.imageryMenu.refreshOffsetMenu();
188            Main.map.repaint();
189        }
190    }
191
192    public class OffsetAction extends AbstractAction implements LayerAction {
193        @Override
194        public void actionPerformed(ActionEvent e) {
195        }
196
197        @Override
198        public Component createMenuComponent() {
199            return getOffsetMenuItem();
200        }
201
202        @Override
203        public boolean supportLayers(List<Layer> layers) {
204            return false;
205        }
206    }
207
208    public JMenuItem getOffsetMenuItem() {
209        JMenu subMenu = new JMenu(trc("layer", "Offset"));
210        subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
211        return (JMenuItem) getOffsetMenuItem(subMenu);
212    }
213
214    public JComponent getOffsetMenuItem(JComponent subMenu) {
215        JMenuItem adjustMenuItem = new JMenuItem(adjustAction);
216        if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem;
217
218        subMenu.add(adjustMenuItem);
219        subMenu.add(new JSeparator());
220        boolean hasBookmarks = false;
221        int menuItemHeight = 0;
222        for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
223            if (!b.isUsable(this)) {
224                continue;
225            }
226            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
227            if (Utils.equalsEpsilon(b.dx, dx) && Utils.equalsEpsilon(b.dy, dy)) {
228                item.setSelected(true);
229            }
230            subMenu.add(item);
231            menuItemHeight = item.getPreferredSize().height;
232            hasBookmarks = true;
233        }
234        if (menuItemHeight > 0) {
235            if (subMenu instanceof JMenu) {
236                MenuScroller.setScrollerFor((JMenu) subMenu);
237            } else if (subMenu instanceof JPopupMenu) {
238                MenuScroller.setScrollerFor((JPopupMenu) subMenu);
239            }
240        }
241        return hasBookmarks ? subMenu : adjustMenuItem;
242    }
243
244    public ImageProcessor createSharpener(int sharpenLevel) {
245        final Kernel kernel;
246        if (sharpenLevel == 1) {
247            kernel = new Kernel(3, 3, new float[]{-0.25f, -0.5f, -0.25f, -0.5f, 4, -0.5f, -0.25f, -0.5f, -0.25f});
248        } else if (sharpenLevel == 2) {
249            kernel = new Kernel(3, 3, new float[]{-0.5f, -1, -0.5f, -1, 7, -1, -0.5f, -1, -0.5f});
250        } else {
251            return null;
252        }
253        BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
254        return createImageProcessor(op, false);
255    }
256
257    /**
258     * An image processor which adjusts the gamma value of an image.
259     */
260    public static class GammaImageProcessor implements ImageProcessor {
261        private double gamma = 1;
262        final short[] gammaChange = new short[256];
263        private final LookupOp op3 = new LookupOp(
264                new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange}), null);
265        private final LookupOp op4 = new LookupOp(
266                new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange, gammaChange}), null);
267
268        /**
269         * Returns the currently set gamma value.
270         * @return the currently set gamma value
271         */
272        public double getGamma() {
273            return gamma;
274        }
275
276        /**
277         * Sets a new gamma value, {@code 1} stands for no correction.
278         * @param gamma new gamma value
279         */
280        public void setGamma(double gamma) {
281            this.gamma = gamma;
282            for (int i = 0; i < 256; i++) {
283                gammaChange[i] = (short) (255 * Math.pow(i / 255., gamma));
284            }
285        }
286
287        @Override
288        public BufferedImage process(BufferedImage image) {
289            if (gamma == 1) {
290                return image;
291            }
292            try {
293                final int bands = image.getRaster().getNumBands();
294                if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 3) {
295                    return op3.filter(image, null);
296                } else if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 4) {
297                    return op4.filter(image, null);
298                }
299            } catch (IllegalArgumentException ignore) {
300                if (Main.isTraceEnabled()) {
301                    Main.trace(ignore.getMessage());
302                }
303            }
304            final int type = image.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
305            final BufferedImage to = new BufferedImage(image.getWidth(), image.getHeight(), type);
306            to.getGraphics().drawImage(image, 0, 0, null);
307            return process(to);
308        }
309    }
310
311    /**
312     * Returns the currently set gamma value.
313     * @return the currently set gamma value
314     */
315    public double getGamma() {
316        return gammaImageProcessor.getGamma();
317    }
318
319    /**
320     * Sets a new gamma value, {@code 1} stands for no correction.
321     * @param gamma new gamma value
322     */
323    public void setGamma(double gamma) {
324        gammaImageProcessor.setGamma(gamma);
325    }
326
327    /**
328     * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
329     *
330     * @param processor that processes the image
331     *
332     * @return true if processor was added, false otherwise
333     */
334    public boolean addImageProcessor(ImageProcessor processor) {
335        return processor != null && imageProcessors.add(processor);
336    }
337
338    /**
339     * This method removes given {@link ImageProcessor} from this layer
340     *
341     * @param processor which is needed to be removed
342     *
343     * @return true if processor was removed
344     */
345    public boolean removeImageProcessor(ImageProcessor processor) {
346        return imageProcessors.remove(processor);
347    }
348
349    /**
350     * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
351     * @param op the {@link BufferedImageOp}
352     * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
353     *                (the {@code op} needs to support this!)
354     * @return the {@link ImageProcessor} wrapper
355     */
356    public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
357        return new ImageProcessor() {
358            @Override
359            public BufferedImage process(BufferedImage image) {
360                return op.filter(image, inPlace ? image : null);
361            }
362        };
363    }
364
365    /**
366     * This method gets all {@link ImageProcessor}s of the layer
367     *
368     * @return list of image processors without removed one
369     */
370    public List<ImageProcessor> getImageProcessors() {
371        return imageProcessors;
372    }
373
374    /**
375     * Applies all the chosen {@link ImageProcessor}s to the image
376     *
377     * @param img - image which should be changed
378     *
379     * @return the new changed image
380     */
381    public BufferedImage applyImageProcessors(BufferedImage img) {
382        for (ImageProcessor processor : imageProcessors) {
383            img = processor.process(img);
384        }
385        return img;
386    }
387
388    /**
389     * Draws a red error tile when imagery tile cannot be fetched.
390     * @param img The buffered image
391     * @param message Additional error message to display
392     */
393    public void drawErrorTile(BufferedImage img, String message) {
394        Graphics2D g = (Graphics2D) img.getGraphics();
395        g.setColor(Color.RED);
396        g.fillRect(0, 0, img.getWidth(), img.getHeight());
397        g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(24.0f));
398        g.setColor(Color.BLACK);
399
400        String text = tr("ERROR");
401        g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, g.getFontMetrics().getHeight()+5);
402        if (message != null) {
403            float drawPosY = 2.5f*g.getFontMetrics().getHeight()+10;
404            if (!message.contains(" ")) {
405                g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(18.0f));
406                g.drawString(message, 5, (int) drawPosY);
407            } else {
408                // Draw message on several lines
409                Map<TextAttribute, Object> map = new HashMap<>();
410                map.put(TextAttribute.FAMILY, "Serif");
411                map.put(TextAttribute.SIZE, new Float(18.0));
412                AttributedString vanGogh = new AttributedString(message, map);
413                // Create a new LineBreakMeasurer from the text
414                AttributedCharacterIterator paragraph = vanGogh.getIterator();
415                int paragraphStart = paragraph.getBeginIndex();
416                int paragraphEnd = paragraph.getEndIndex();
417                FontRenderContext frc = g.getFontRenderContext();
418                LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
419                // Set break width to width of image with some margin
420                float breakWidth = img.getWidth()-10;
421                // Set position to the index of the first character in the text
422                lineMeasurer.setPosition(paragraphStart);
423                // Get lines until the entire paragraph has been displayed
424                while (lineMeasurer.getPosition() < paragraphEnd) {
425                    // Retrieve next layout
426                    TextLayout layout = lineMeasurer.nextLayout(breakWidth);
427
428                    // Compute pen x position
429                    float drawPosX = layout.isLeftToRight() ? 0 : breakWidth - layout.getAdvance();
430
431                    // Move y-coordinate by the ascent of the layout
432                    drawPosY += layout.getAscent();
433
434                    // Draw the TextLayout at (drawPosX, drawPosY)
435                    layout.draw(g, drawPosX, drawPosY);
436
437                    // Move y-coordinate in preparation for next layout
438                    drawPosY += layout.getDescent() + layout.getLeading();
439                }
440            }
441        }
442    }
443
444    @Override
445    public void destroy() {
446        super.destroy();
447        adjustAction.destroy();
448    }
449}