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.event.ActionEvent;
012import java.awt.image.BufferedImage;
013import java.awt.image.BufferedImageOp;
014import java.util.ArrayList;
015import java.util.List;
016
017import javax.swing.AbstractAction;
018import javax.swing.Action;
019import javax.swing.Icon;
020import javax.swing.JCheckBoxMenuItem;
021import javax.swing.JComponent;
022import javax.swing.JLabel;
023import javax.swing.JMenu;
024import javax.swing.JMenuItem;
025import javax.swing.JPanel;
026import javax.swing.JPopupMenu;
027import javax.swing.JSeparator;
028
029import org.openstreetmap.josm.Main;
030import org.openstreetmap.josm.data.ProjectionBounds;
031import org.openstreetmap.josm.data.imagery.ImageryInfo;
032import org.openstreetmap.josm.data.imagery.OffsetBookmark;
033import org.openstreetmap.josm.data.preferences.ColorProperty;
034import org.openstreetmap.josm.data.preferences.IntegerProperty;
035import org.openstreetmap.josm.gui.MenuScroller;
036import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
037import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
038import org.openstreetmap.josm.gui.widgets.UrlLabel;
039import org.openstreetmap.josm.tools.GBC;
040import org.openstreetmap.josm.tools.ImageProvider;
041import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
042import org.openstreetmap.josm.tools.Utils;
043
044public abstract class ImageryLayer extends Layer {
045
046    public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white);
047    public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
048    public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
049
050    private final List<ImageProcessor> imageProcessors = new ArrayList<>();
051
052    public static Color getFadeColor() {
053        return PROP_FADE_COLOR.get();
054    }
055
056    public static Color getFadeColorWithAlpha() {
057        Color c = PROP_FADE_COLOR.get();
058        return new Color(c.getRed(), c.getGreen(), c.getBlue(), PROP_FADE_AMOUNT.get()*255/100);
059    }
060
061    protected final ImageryInfo info;
062
063    protected Icon icon;
064
065    private final ImageryFilterSettings filterSettings = new ImageryFilterSettings();
066
067    /**
068     * Constructs a new {@code ImageryLayer}.
069     * @param info imagery info
070     */
071    public ImageryLayer(ImageryInfo info) {
072        super(info.getName());
073        this.info = info;
074        if (info.getIcon() != null) {
075            icon = new ImageProvider(info.getIcon()).setOptional(true).
076                    setMaxSize(ImageSizes.LAYER).get();
077        }
078        if (icon == null) {
079            icon = ImageProvider.get("imagery_small");
080        }
081        for (ImageProcessor processor : filterSettings.getProcessors()) {
082            addImageProcessor(processor);
083        }
084        filterSettings.setSharpenLevel(1 + PROP_SHARPEN_LEVEL.get() / 2f);
085    }
086
087    public double getPPD() {
088        if (!Main.isDisplayingMapView())
089            return Main.getProjection().getDefaultZoomInPPD();
090        ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
091        return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast);
092    }
093
094    /**
095     * Gets the x displacement of this layer.
096     * To be removed end of 2016
097     * @return The x displacement.
098     * @deprecated Use {@link TileSourceDisplaySettings#getDx()}
099     */
100    @Deprecated
101    public double getDx() {
102        // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate.
103        return 0;
104    }
105
106    /**
107     * Gets the y displacement of this layer.
108     * To be removed end of 2016
109     * @return The y displacement.
110     * @deprecated Use {@link TileSourceDisplaySettings#getDy()}
111     */
112    @Deprecated
113    public double getDy() {
114        // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate.
115        return 0;
116    }
117
118    /**
119     * Sets the displacement offset of this layer. The layer is automatically invalidated.
120     * To be removed end of 2016
121     * @param dx The x offset
122     * @param dy The y offset
123     * @deprecated Use {@link TileSourceDisplaySettings}
124     */
125    @Deprecated
126    public void setOffset(double dx, double dy) {
127        // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate.
128    }
129
130    /**
131     * To be removed end of 2016
132     * @param dx deprecated
133     * @param dy deprecated
134     * @deprecated Use {@link TileSourceDisplaySettings}
135     */
136    @Deprecated
137    public void displace(double dx, double dy) {
138        // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate.
139    }
140
141    /**
142     * Returns imagery info.
143     * @return imagery info
144     */
145    public ImageryInfo getInfo() {
146        return info;
147    }
148
149    @Override
150    public Icon getIcon() {
151        return icon;
152    }
153
154    @Override
155    public boolean isMergable(Layer other) {
156        return false;
157    }
158
159    @Override
160    public void mergeFrom(Layer from) {
161    }
162
163    @Override
164    public Object getInfoComponent() {
165        JPanel panel = new JPanel(new GridBagLayout());
166        panel.add(new JLabel(getToolTipText()), GBC.eol());
167        if (info != null) {
168            String url = info.getUrl();
169            if (url != null) {
170                panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
171                panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
172            }
173        }
174        return panel;
175    }
176
177    public static ImageryLayer create(ImageryInfo info) {
178        switch(info.getImageryType()) {
179        case WMS:
180        case HTML:
181            return new WMSLayer(info);
182        case WMTS:
183            return new WMTSLayer(info);
184        case TMS:
185        case BING:
186        case SCANEX:
187            return new TMSLayer(info);
188        default:
189            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
190        }
191    }
192
193    class ApplyOffsetAction extends AbstractAction {
194        private final transient OffsetBookmark b;
195
196        ApplyOffsetAction(OffsetBookmark b) {
197            super(b.name);
198            this.b = b;
199        }
200
201        @Override
202        public void actionPerformed(ActionEvent ev) {
203            setOffset(b.dx, b.dy);
204            Main.main.menu.imageryMenu.refreshOffsetMenu();
205            Main.map.repaint();
206        }
207    }
208
209    public class OffsetAction extends AbstractAction implements LayerAction {
210        @Override
211        public void actionPerformed(ActionEvent e) {
212            // Do nothing
213        }
214
215        @Override
216        public Component createMenuComponent() {
217            return getOffsetMenuItem();
218        }
219
220        @Override
221        public boolean supportLayers(List<Layer> layers) {
222            return false;
223        }
224    }
225
226    public JMenuItem getOffsetMenuItem() {
227        JMenu subMenu = new JMenu(trc("layer", "Offset"));
228        subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
229        return (JMenuItem) getOffsetMenuItem(subMenu);
230    }
231
232    public JComponent getOffsetMenuItem(JComponent subMenu) {
233        JMenuItem adjustMenuItem = new JMenuItem(getAdjustAction());
234        if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem;
235
236        subMenu.add(adjustMenuItem);
237        subMenu.add(new JSeparator());
238        boolean hasBookmarks = false;
239        int menuItemHeight = 0;
240        for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
241            if (!b.isUsable(this)) {
242                continue;
243            }
244            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
245            if (Utils.equalsEpsilon(b.dx, getDx()) && Utils.equalsEpsilon(b.dy, getDy())) {
246                item.setSelected(true);
247            }
248            subMenu.add(item);
249            menuItemHeight = item.getPreferredSize().height;
250            hasBookmarks = true;
251        }
252        if (menuItemHeight > 0) {
253            if (subMenu instanceof JMenu) {
254                MenuScroller.setScrollerFor((JMenu) subMenu);
255            } else if (subMenu instanceof JPopupMenu) {
256                MenuScroller.setScrollerFor((JPopupMenu) subMenu);
257            }
258        }
259        return hasBookmarks ? subMenu : adjustMenuItem;
260    }
261
262    protected abstract Action getAdjustAction();
263
264    /**
265     * Gets the settings for the filter that is applied to this layer.
266     * @return The filter settings.
267     * @since 10547
268     */
269    public ImageryFilterSettings getFilterSettings() {
270        return filterSettings;
271    }
272
273    /**
274     * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
275     *
276     * @param processor that processes the image
277     *
278     * @return true if processor was added, false otherwise
279     */
280    public boolean addImageProcessor(ImageProcessor processor) {
281        return processor != null && imageProcessors.add(processor);
282    }
283
284    /**
285     * This method removes given {@link ImageProcessor} from this layer
286     *
287     * @param processor which is needed to be removed
288     *
289     * @return true if processor was removed
290     */
291    public boolean removeImageProcessor(ImageProcessor processor) {
292        return imageProcessors.remove(processor);
293    }
294
295    /**
296     * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
297     * @param op the {@link BufferedImageOp}
298     * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
299     *                (the {@code op} needs to support this!)
300     * @return the {@link ImageProcessor} wrapper
301     */
302    public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
303        return image -> op.filter(image, inPlace ? image : null);
304    }
305
306    /**
307     * This method gets all {@link ImageProcessor}s of the layer
308     *
309     * @return list of image processors without removed one
310     */
311    public List<ImageProcessor> getImageProcessors() {
312        return imageProcessors;
313    }
314
315    /**
316     * Applies all the chosen {@link ImageProcessor}s to the image
317     *
318     * @param img - image which should be changed
319     *
320     * @return the new changed image
321     */
322    public BufferedImage applyImageProcessors(BufferedImage img) {
323        for (ImageProcessor processor : imageProcessors) {
324            img = processor.process(img);
325        }
326        return img;
327    }
328
329    @Override
330    public String toString() {
331        return getClass().getSimpleName() + " [info=" + info + ']';
332    }
333}