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.GridBagLayout;
011import java.awt.Rectangle;
012import java.awt.RenderingHints;
013import java.awt.Transparency;
014import java.awt.event.ActionEvent;
015import java.awt.geom.Point2D;
016import java.awt.geom.Rectangle2D;
017import java.awt.image.BufferedImage;
018import java.awt.image.BufferedImageOp;
019import java.awt.image.ColorModel;
020import java.awt.image.ConvolveOp;
021import java.awt.image.DataBuffer;
022import java.awt.image.DataBufferByte;
023import java.awt.image.Kernel;
024import java.awt.image.LookupOp;
025import java.awt.image.ShortLookupTable;
026import java.util.ArrayList;
027import java.util.List;
028
029import javax.swing.AbstractAction;
030import javax.swing.Icon;
031import javax.swing.JCheckBoxMenuItem;
032import javax.swing.JComponent;
033import javax.swing.JLabel;
034import javax.swing.JMenu;
035import javax.swing.JMenuItem;
036import javax.swing.JPanel;
037import javax.swing.JPopupMenu;
038import javax.swing.JSeparator;
039
040import org.openstreetmap.josm.Main;
041import org.openstreetmap.josm.actions.ImageryAdjustAction;
042import org.openstreetmap.josm.data.ProjectionBounds;
043import org.openstreetmap.josm.data.imagery.ImageryInfo;
044import org.openstreetmap.josm.data.imagery.OffsetBookmark;
045import org.openstreetmap.josm.data.preferences.ColorProperty;
046import org.openstreetmap.josm.data.preferences.IntegerProperty;
047import org.openstreetmap.josm.gui.MenuScroller;
048import org.openstreetmap.josm.gui.widgets.UrlLabel;
049import org.openstreetmap.josm.tools.GBC;
050import org.openstreetmap.josm.tools.ImageProvider;
051import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
052import org.openstreetmap.josm.tools.Utils;
053
054public abstract class ImageryLayer extends Layer {
055
056    public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white);
057    public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
058    public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
059
060    private final List<ImageProcessor> imageProcessors = new ArrayList<>();
061
062    public static Color getFadeColor() {
063        return PROP_FADE_COLOR.get();
064    }
065
066    public static Color getFadeColorWithAlpha() {
067        Color c = PROP_FADE_COLOR.get();
068        return new Color(c.getRed(), c.getGreen(), c.getBlue(), PROP_FADE_AMOUNT.get()*255/100);
069    }
070
071    protected final ImageryInfo info;
072
073    protected Icon icon;
074
075    protected double dx;
076    protected double dy;
077
078    protected GammaImageProcessor gammaImageProcessor = new GammaImageProcessor();
079    protected SharpenImageProcessor sharpenImageProcessor = new SharpenImageProcessor();
080    protected ColorfulImageProcessor collorfulnessImageProcessor = new ColorfulImageProcessor();
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                    setMaxSize(ImageSizes.LAYER).get();
094        }
095        if (icon == null) {
096            icon = ImageProvider.get("imagery_small");
097        }
098        addImageProcessor(collorfulnessImageProcessor);
099        addImageProcessor(gammaImageProcessor);
100        addImageProcessor(sharpenImageProcessor);
101        sharpenImageProcessor.setSharpenLevel(1 + PROP_SHARPEN_LEVEL.get() / 2f);
102    }
103
104    public double getPPD() {
105        if (!Main.isDisplayingMapView())
106            return Main.getProjection().getDefaultZoomInPPD();
107        ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
108        return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast);
109    }
110
111    public double getDx() {
112        return dx;
113    }
114
115    public double getDy() {
116        return dy;
117    }
118
119    public void setOffset(double dx, double dy) {
120        this.dx = dx;
121        this.dy = dy;
122    }
123
124    public void displace(double dx, double dy) {
125        this.dx += dx;
126        this.dy += dy;
127        setOffset(this.dx, this.dy);
128    }
129
130    /**
131     * Returns imagery info.
132     * @return imagery info
133     */
134    public ImageryInfo getInfo() {
135        return info;
136    }
137
138    @Override
139    public Icon getIcon() {
140        return icon;
141    }
142
143    @Override
144    public boolean isMergable(Layer other) {
145        return false;
146    }
147
148    @Override
149    public void mergeFrom(Layer from) {
150    }
151
152    @Override
153    public Object getInfoComponent() {
154        JPanel panel = new JPanel(new GridBagLayout());
155        panel.add(new JLabel(getToolTipText()), GBC.eol());
156        if (info != null) {
157            String url = info.getUrl();
158            if (url != null) {
159                panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
160                panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
161            }
162            if (dx != 0 || dy != 0) {
163                panel.add(new JLabel(tr("Offset: ") + dx + ';' + dy), GBC.eol().insets(0, 5, 10, 0));
164            }
165        }
166        return panel;
167    }
168
169    public static ImageryLayer create(ImageryInfo info) {
170        switch(info.getImageryType()) {
171        case WMS:
172        case HTML:
173            return new WMSLayer(info);
174        case WMTS:
175            return new WMTSLayer(info);
176        case TMS:
177        case BING:
178        case SCANEX:
179            return new TMSLayer(info);
180        default:
181            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
182        }
183    }
184
185    class ApplyOffsetAction extends AbstractAction {
186        private final transient OffsetBookmark b;
187
188        ApplyOffsetAction(OffsetBookmark b) {
189            super(b.name);
190            this.b = b;
191        }
192
193        @Override
194        public void actionPerformed(ActionEvent ev) {
195            setOffset(b.dx, b.dy);
196            Main.main.menu.imageryMenu.refreshOffsetMenu();
197            Main.map.repaint();
198        }
199    }
200
201    public class OffsetAction extends AbstractAction implements LayerAction {
202        @Override
203        public void actionPerformed(ActionEvent e) {
204            // Do nothing
205        }
206
207        @Override
208        public Component createMenuComponent() {
209            return getOffsetMenuItem();
210        }
211
212        @Override
213        public boolean supportLayers(List<Layer> layers) {
214            return false;
215        }
216    }
217
218    public JMenuItem getOffsetMenuItem() {
219        JMenu subMenu = new JMenu(trc("layer", "Offset"));
220        subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
221        return (JMenuItem) getOffsetMenuItem(subMenu);
222    }
223
224    public JComponent getOffsetMenuItem(JComponent subMenu) {
225        JMenuItem adjustMenuItem = new JMenuItem(adjustAction);
226        if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem;
227
228        subMenu.add(adjustMenuItem);
229        subMenu.add(new JSeparator());
230        boolean hasBookmarks = false;
231        int menuItemHeight = 0;
232        for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
233            if (!b.isUsable(this)) {
234                continue;
235            }
236            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
237            if (Utils.equalsEpsilon(b.dx, dx) && Utils.equalsEpsilon(b.dy, dy)) {
238                item.setSelected(true);
239            }
240            subMenu.add(item);
241            menuItemHeight = item.getPreferredSize().height;
242            hasBookmarks = true;
243        }
244        if (menuItemHeight > 0) {
245            if (subMenu instanceof JMenu) {
246                MenuScroller.setScrollerFor((JMenu) subMenu);
247            } else if (subMenu instanceof JPopupMenu) {
248                MenuScroller.setScrollerFor((JPopupMenu) subMenu);
249            }
250        }
251        return hasBookmarks ? subMenu : adjustMenuItem;
252    }
253
254    /**
255     * An image processor which adjusts the gamma value of an image.
256     */
257    public static class GammaImageProcessor implements ImageProcessor {
258        private double gamma = 1;
259        final short[] gammaChange = new short[256];
260        private final LookupOp op3 = new LookupOp(
261                new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange}), null);
262        private final LookupOp op4 = new LookupOp(
263                new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange, gammaChange}), null);
264
265        /**
266         * Returns the currently set gamma value.
267         * @return the currently set gamma value
268         */
269        public double getGamma() {
270            return gamma;
271        }
272
273        /**
274         * Sets a new gamma value, {@code 1} stands for no correction.
275         * @param gamma new gamma value
276         */
277        public void setGamma(double gamma) {
278            this.gamma = gamma;
279            for (int i = 0; i < 256; i++) {
280                gammaChange[i] = (short) (255 * Math.pow(i / 255., gamma));
281            }
282        }
283
284        @Override
285        public BufferedImage process(BufferedImage image) {
286            if (gamma == 1) {
287                return image;
288            }
289            try {
290                final int bands = image.getRaster().getNumBands();
291                if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 3) {
292                    return op3.filter(image, null);
293                } else if (image.getType() != BufferedImage.TYPE_CUSTOM && bands == 4) {
294                    return op4.filter(image, null);
295                }
296            } catch (IllegalArgumentException ignore) {
297                if (Main.isTraceEnabled()) {
298                    Main.trace(ignore.getMessage());
299                }
300            }
301            final int type = image.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
302            final BufferedImage to = new BufferedImage(image.getWidth(), image.getHeight(), type);
303            to.getGraphics().drawImage(image, 0, 0, null);
304            return process(to);
305        }
306
307        @Override
308        public String toString() {
309            return "GammaImageProcessor [gamma=" + gamma + ']';
310        }
311    }
312
313    /**
314     * Sharpens or blurs the image, depending on the sharpen value.
315     * <p>
316     * A positive sharpen level means that we sharpen the image.
317     * <p>
318     * A negative sharpen level let's us blur the image. -1 is the most useful value there.
319     *
320     * @author Michael Zangl
321     */
322    public static class SharpenImageProcessor implements ImageProcessor {
323        private float sharpenLevel;
324        private ConvolveOp op;
325
326        private static float[] KERNEL_IDENTITY = new float[] {
327            0, 0, 0,
328            0, 1, 0,
329            0, 0, 0
330        };
331
332        private static float[] KERNEL_BLUR = new float[] {
333            1f / 16, 2f / 16, 1f / 16,
334            2f / 16, 4f / 16, 2f / 16,
335            1f / 16, 2f / 16, 1f / 16
336        };
337
338        private static float[] KERNEL_SHARPEN = new float[] {
339            -.5f, -1f, -.5f,
340             -1f,  7,  -1f,
341            -.5f, -1f, -.5f
342        };
343
344        /**
345         * Gets the current sharpen level.
346         * @return The level.
347         */
348        public float getSharpenLevel() {
349            return sharpenLevel;
350        }
351
352        /**
353         * Sets the sharpening level.
354         * @param sharpenLevel The level. Clamped to be positive or 0.
355         */
356        public void setSharpenLevel(float sharpenLevel) {
357            if (sharpenLevel < 0) {
358                this.sharpenLevel = 0;
359            } else {
360                this.sharpenLevel = sharpenLevel;
361            }
362
363            if (this.sharpenLevel < 0.95) {
364                op = generateMixed(this.sharpenLevel, KERNEL_IDENTITY, KERNEL_BLUR);
365            } else if (this.sharpenLevel > 1.05) {
366                op = generateMixed(this.sharpenLevel - 1, KERNEL_SHARPEN, KERNEL_IDENTITY);
367            } else {
368                op = null;
369            }
370        }
371
372        private ConvolveOp generateMixed(float aFactor, float[] a, float[] b) {
373            if (a.length != 9 || b.length != 9) {
374                throw new IllegalArgumentException("Illegal kernel array length.");
375            }
376            float[] values = new float[9];
377            for (int i = 0; i < values.length; i++) {
378                values[i] = aFactor * a[i] + (1 - aFactor) * b[i];
379            }
380            return new ConvolveOp(new Kernel(3, 3, values), ConvolveOp.EDGE_NO_OP, null);
381        }
382
383        @Override
384        public BufferedImage process(BufferedImage image) {
385            if (op != null) {
386                return op.filter(image, null);
387            } else {
388                return image;
389            }
390        }
391
392        @Override
393        public String toString() {
394            return "SharpenImageProcessor [sharpenLevel=" + sharpenLevel + ']';
395        }
396    }
397
398    /**
399     * Adds or removes the colorfulness of the image.
400     *
401     * @author Michael Zangl
402     */
403    public static class ColorfulImageProcessor implements ImageProcessor {
404        private ColorfulFilter op;
405        private double colorfulness = 1;
406
407        /**
408         * Gets the colorfulness value.
409         * @return The value
410         */
411        public double getColorfulness() {
412            return colorfulness;
413        }
414
415        /**
416         * Sets the colorfulness value. Clamps it to 0+
417         * @param colorfulness The value
418         */
419        public void setColorfulness(double colorfulness) {
420            if (colorfulness < 0) {
421                this.colorfulness = 0;
422            } else {
423                this.colorfulness = colorfulness;
424            }
425
426            if (this.colorfulness < .95 || this.colorfulness > 1.05) {
427                op = new ColorfulFilter(this.colorfulness);
428            } else {
429                op = null;
430            }
431        }
432
433        @Override
434        public BufferedImage process(BufferedImage image) {
435            if (op != null) {
436                return op.filter(image, null);
437            } else {
438                return image;
439            }
440        }
441
442        @Override
443        public String toString() {
444            return "ColorfulImageProcessor [colorfulness=" + colorfulness + ']';
445        }
446    }
447
448    private static class ColorfulFilter implements BufferedImageOp {
449        private final double colorfulness;
450
451        /**
452         * Create a new colorful filter.
453         * @param colorfulness The colorfulness as defined in the {@link ColorfulImageProcessor} class.
454         */
455        ColorfulFilter(double colorfulness) {
456            this.colorfulness = colorfulness;
457        }
458
459        @Override
460        public BufferedImage filter(BufferedImage src, BufferedImage dest) {
461            if (src.getWidth() == 0 || src.getHeight() == 0) {
462                return src;
463            }
464
465            if (dest == null) {
466                dest = createCompatibleDestImage(src, null);
467            }
468            DataBuffer srcBuffer = src.getRaster().getDataBuffer();
469            DataBuffer destBuffer = dest.getRaster().getDataBuffer();
470            if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) {
471                Main.trace("Cannot apply color filter: Images do not use DataBufferByte.");
472                return src;
473            }
474
475            int type = src.getType();
476            if (type != dest.getType()) {
477                Main.trace("Cannot apply color filter: Src / Dest differ in type (" + type + '/' + dest.getType() + ')');
478                return src;
479            }
480            int redOffset, greenOffset, blueOffset, alphaOffset = 0;
481            switch (type) {
482            case BufferedImage.TYPE_3BYTE_BGR:
483                blueOffset = 0;
484                greenOffset = 1;
485                redOffset = 2;
486                break;
487            case BufferedImage.TYPE_4BYTE_ABGR:
488            case BufferedImage.TYPE_4BYTE_ABGR_PRE:
489                blueOffset = 1;
490                greenOffset = 2;
491                redOffset = 3;
492                break;
493            case BufferedImage.TYPE_INT_ARGB:
494            case BufferedImage.TYPE_INT_ARGB_PRE:
495                redOffset = 0;
496                greenOffset = 1;
497                blueOffset = 2;
498                alphaOffset = 3;
499                break;
500            default:
501                Main.trace("Cannot apply color filter: Source image is of wrong type (" + type + ").");
502                return src;
503            }
504            doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset,
505                    alphaOffset, src.getAlphaRaster() != null);
506            return dest;
507        }
508
509        private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset,
510                int alphaOffset, boolean hasAlpha) {
511            byte[] srcPixels = src.getData();
512            byte[] destPixels = dest.getData();
513            if (srcPixels.length != destPixels.length) {
514                Main.trace("Cannot apply color filter: Source/Dest lengths differ.");
515                return;
516            }
517            int entries = hasAlpha ? 4 : 3;
518            for (int i = 0; i < srcPixels.length; i += entries) {
519                int r = srcPixels[i + redOffset] & 0xff;
520                int g = srcPixels[i + greenOffset] & 0xff;
521                int b = srcPixels[i + blueOffset] & 0xff;
522                double luminosity = r * .21d + g * .72d + b * .07d;
523                destPixels[i + redOffset] = mix(r, luminosity);
524                destPixels[i + greenOffset] = mix(g, luminosity);
525                destPixels[i + blueOffset] = mix(b, luminosity);
526                if (hasAlpha) {
527                    destPixels[i + alphaOffset] = srcPixels[i + alphaOffset];
528                }
529            }
530        }
531
532        private byte mix(int color, double luminosity) {
533            int val = (int) (colorfulness * color +  (1 - colorfulness) * luminosity);
534            if (val < 0) {
535                return 0;
536            } else if (val > 0xff) {
537                return (byte) 0xff;
538            } else {
539                return (byte) val;
540            }
541        }
542
543        @Override
544        public Rectangle2D getBounds2D(BufferedImage src) {
545            return new Rectangle(src.getWidth(), src.getHeight());
546        }
547
548        @Override
549        public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
550            return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
551        }
552
553        @Override
554        public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
555            return (Point2D) srcPt.clone();
556        }
557
558        @Override
559        public RenderingHints getRenderingHints() {
560            return null;
561        }
562
563    }
564
565    /**
566     * Returns the currently set gamma value.
567     * @return the currently set gamma value
568     */
569    public double getGamma() {
570        return gammaImageProcessor.getGamma();
571    }
572
573    /**
574     * Sets a new gamma value, {@code 1} stands for no correction.
575     * @param gamma new gamma value
576     */
577    public void setGamma(double gamma) {
578        gammaImageProcessor.setGamma(gamma);
579    }
580
581    /**
582     * Gets the current sharpen level.
583     * @return The sharpen level.
584     */
585    public double getSharpenLevel() {
586        return sharpenImageProcessor.getSharpenLevel();
587    }
588
589    /**
590     * Sets the sharpen level for the layer.
591     * <code>1</code> means no change in sharpness.
592     * Values in range 0..1 blur the image.
593     * Values above 1 are used to sharpen the image.
594     * @param sharpenLevel The sharpen level.
595     */
596    public void setSharpenLevel(double sharpenLevel) {
597        sharpenImageProcessor.setSharpenLevel((float) sharpenLevel);
598    }
599
600    /**
601     * Gets the colorfulness of this image.
602     * @return The colorfulness
603     */
604    public double getColorfulness() {
605        return collorfulnessImageProcessor.getColorfulness();
606    }
607
608    /**
609     * Sets the colorfulness of this image.
610     * 0 means grayscale.
611     * 1 means normal colorfulness.
612     * Values greater than 1 are allowed.
613     * @param colorfulness The colorfulness.
614     */
615    public void setColorfulness(double colorfulness) {
616        collorfulnessImageProcessor.setColorfulness(colorfulness);
617    }
618
619    /**
620     * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
621     *
622     * @param processor that processes the image
623     *
624     * @return true if processor was added, false otherwise
625     */
626    public boolean addImageProcessor(ImageProcessor processor) {
627        return processor != null && imageProcessors.add(processor);
628    }
629
630    /**
631     * This method removes given {@link ImageProcessor} from this layer
632     *
633     * @param processor which is needed to be removed
634     *
635     * @return true if processor was removed
636     */
637    public boolean removeImageProcessor(ImageProcessor processor) {
638        return imageProcessors.remove(processor);
639    }
640
641    /**
642     * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
643     * @param op the {@link BufferedImageOp}
644     * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
645     *                (the {@code op} needs to support this!)
646     * @return the {@link ImageProcessor} wrapper
647     */
648    public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
649        return new ImageProcessor() {
650            @Override
651            public BufferedImage process(BufferedImage image) {
652                return op.filter(image, inPlace ? image : null);
653            }
654        };
655    }
656
657    /**
658     * This method gets all {@link ImageProcessor}s of the layer
659     *
660     * @return list of image processors without removed one
661     */
662    public List<ImageProcessor> getImageProcessors() {
663        return imageProcessors;
664    }
665
666    /**
667     * Applies all the chosen {@link ImageProcessor}s to the image
668     *
669     * @param img - image which should be changed
670     *
671     * @return the new changed image
672     */
673    public BufferedImage applyImageProcessors(BufferedImage img) {
674        for (ImageProcessor processor : imageProcessors) {
675            img = processor.process(img);
676        }
677        return img;
678    }
679
680    @Override
681    public void destroy() {
682        super.destroy();
683        adjustAction.destroy();
684    }
685}