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