001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trc;
006
007import java.awt.Component;
008import java.awt.GridBagLayout;
009import java.awt.event.ActionEvent;
010import java.awt.image.BufferedImage;
011import java.awt.image.BufferedImageOp;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.List;
015import java.util.Locale;
016
017import javax.swing.AbstractAction;
018import javax.swing.Action;
019import javax.swing.BorderFactory;
020import javax.swing.Icon;
021import javax.swing.JCheckBoxMenuItem;
022import javax.swing.JComponent;
023import javax.swing.JLabel;
024import javax.swing.JMenu;
025import javax.swing.JMenuItem;
026import javax.swing.JPanel;
027import javax.swing.JPopupMenu;
028import javax.swing.JSeparator;
029import javax.swing.JTextField;
030
031import org.openstreetmap.josm.Main;
032import org.openstreetmap.josm.data.ProjectionBounds;
033import org.openstreetmap.josm.data.imagery.ImageryInfo;
034import org.openstreetmap.josm.data.imagery.OffsetBookmark;
035import org.openstreetmap.josm.data.preferences.IntegerProperty;
036import org.openstreetmap.josm.gui.MainApplication;
037import org.openstreetmap.josm.gui.MapView;
038import org.openstreetmap.josm.gui.MenuScroller;
039import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
040import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
041import org.openstreetmap.josm.gui.widgets.UrlLabel;
042import org.openstreetmap.josm.tools.GBC;
043import org.openstreetmap.josm.tools.ImageProcessor;
044import org.openstreetmap.josm.tools.ImageProvider;
045import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
046
047/**
048 * Abstract base class for background imagery layers ({@link WMSLayer}, {@link TMSLayer}, {@link WMTSLayer}).
049 *
050 * Handles some common tasks, like image filters, image processors, etc.
051 */
052public abstract class ImageryLayer extends Layer {
053
054    /**
055     * The default value for the sharpen filter for each imagery layer.
056     */
057    public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
058
059    private final List<ImageProcessor> imageProcessors = new ArrayList<>();
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 (!MainApplication.isDisplayingMapView())
089            return Main.getProjection().getDefaultZoomInPPD();
090        MapView mapView = MainApplication.getMap().mapView;
091        ProjectionBounds bounds = mapView.getProjectionBounds();
092        return mapView.getWidth() / (bounds.maxEast - bounds.minEast);
093    }
094
095    /**
096     * Gets the x displacement of this layer.
097     * To be removed end of 2016
098     * @return The x displacement.
099     * @deprecated Use {@link TileSourceDisplaySettings#getDx()}
100     */
101    @Deprecated
102    public double getDx() {
103        // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate.
104        return 0;
105    }
106
107    /**
108     * Gets the y displacement of this layer.
109     * To be removed end of 2016
110     * @return The y displacement.
111     * @deprecated Use {@link TileSourceDisplaySettings#getDy()}
112     */
113    @Deprecated
114    public double getDy() {
115        // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate.
116        return 0;
117    }
118
119    /**
120     * Sets the displacement offset of this layer. The layer is automatically invalidated.
121     * To be removed end of 2016
122     * @param offset the offset bookmark
123     * @deprecated Use {@link TileSourceDisplaySettings}
124     */
125    @Deprecated
126    public void setOffset(OffsetBookmark offset) {
127        // moved to AbstractTileSourceLayer/TileSourceDisplaySettings. Remains until all actions migrate.
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            List<List<String>> content = new ArrayList<>();
158            content.add(Arrays.asList(tr("Name"), info.getName()));
159            content.add(Arrays.asList(tr("Type"), info.getImageryType().getTypeString().toUpperCase(Locale.ENGLISH)));
160            content.add(Arrays.asList(tr("URL"), info.getUrl()));
161            content.add(Arrays.asList(tr("Id"), info.getId() == null ? "-" : info.getId()));
162            if (info.getMinZoom() != 0) {
163                content.add(Arrays.asList(tr("Min. zoom"), Integer.toString(info.getMinZoom())));
164            }
165            if (info.getMaxZoom() != 0) {
166                content.add(Arrays.asList(tr("Max. zoom"), Integer.toString(info.getMaxZoom())));
167            }
168            if (info.getDescription() != null) {
169                content.add(Arrays.asList(tr("Description"), info.getDescription()));
170            }
171            for (List<String> entry: content) {
172                panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
173                panel.add(GBC.glue(5, 0), GBC.std());
174                panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
175            }
176        }
177        return panel;
178    }
179
180    protected JComponent createTextField(String text) {
181        if (text != null && text.matches("https?://.*")) {
182            return new UrlLabel(text);
183        }
184        JTextField ret = new JTextField(text);
185        ret.setEditable(false);
186        ret.setBorder(BorderFactory.createEmptyBorder());
187        return ret;
188    }
189
190    /**
191     * Create a new imagery layer
192     * @param info The imagery info to use as base
193     * @return The created layer
194     */
195    public static ImageryLayer create(ImageryInfo info) {
196        switch(info.getImageryType()) {
197        case WMS:
198            return new WMSLayer(info);
199        case WMTS:
200            return new WMTSLayer(info);
201        case TMS:
202        case BING:
203        case SCANEX:
204            return new TMSLayer(info);
205        default:
206            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
207        }
208    }
209
210    private static class ApplyOffsetAction extends AbstractAction {
211        private final transient OffsetMenuEntry menuEntry;
212
213        ApplyOffsetAction(OffsetMenuEntry menuEntry) {
214            super(menuEntry.getLabel());
215            this.menuEntry = menuEntry;
216        }
217
218        @Override
219        public void actionPerformed(ActionEvent ev) {
220            menuEntry.actionPerformed();
221            //TODO: Use some form of listeners for this.
222            MainApplication.getMenu().imageryMenu.refreshOffsetMenu();
223        }
224    }
225
226    public class OffsetAction extends AbstractAction implements LayerAction {
227        @Override
228        public void actionPerformed(ActionEvent e) {
229            // Do nothing
230        }
231
232        @Override
233        public Component createMenuComponent() {
234            return getOffsetMenuItem();
235        }
236
237        @Override
238        public boolean supportLayers(List<Layer> layers) {
239            return false;
240        }
241    }
242
243    /**
244     * Create the menu item that should be added to the offset menu.
245     * It may have a sub menu of e.g. bookmarks added to it.
246     * @return The menu item to add to the imagery menu.
247     */
248    public JMenuItem getOffsetMenuItem() {
249        JMenu subMenu = new JMenu(trc("layer", "Offset"));
250        subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
251        return (JMenuItem) getOffsetMenuItem(subMenu);
252    }
253
254    /**
255     * Create the submenu or the menu item to set the offset of the layer.
256     *
257     * If only one menu item for this layer exists, it is returned by this method.
258     *
259     * If there are multiple, this method appends them to the subMenu and then returns the reference to the subMenu.
260     * @param subMenu The subMenu to use
261     * @return A single menu item to adjust the layer or the passed subMenu to which the menu items were appended.
262     */
263    public JComponent getOffsetMenuItem(JComponent subMenu) {
264        JMenuItem adjustMenuItem = new JMenuItem(getAdjustAction());
265        List<OffsetMenuEntry> usableBookmarks = getOffsetMenuEntries();
266        if (usableBookmarks.isEmpty()) {
267            return adjustMenuItem;
268        }
269
270        subMenu.add(adjustMenuItem);
271        subMenu.add(new JSeparator());
272        int menuItemHeight = 0;
273        for (OffsetMenuEntry b : usableBookmarks) {
274            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
275            item.setSelected(b.isActive());
276            subMenu.add(item);
277            menuItemHeight = item.getPreferredSize().height;
278        }
279        if (menuItemHeight > 0) {
280            if (subMenu instanceof JMenu) {
281                MenuScroller.setScrollerFor((JMenu) subMenu);
282            } else if (subMenu instanceof JPopupMenu) {
283                MenuScroller.setScrollerFor((JPopupMenu) subMenu);
284            }
285        }
286        return subMenu;
287    }
288
289    protected abstract Action getAdjustAction();
290
291    protected abstract List<OffsetMenuEntry> getOffsetMenuEntries();
292
293    /**
294     * Gets the settings for the filter that is applied to this layer.
295     * @return The filter settings.
296     * @since 10547
297     */
298    public ImageryFilterSettings getFilterSettings() {
299        return filterSettings;
300    }
301
302    /**
303     * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
304     *
305     * @param processor that processes the image
306     *
307     * @return true if processor was added, false otherwise
308     */
309    public boolean addImageProcessor(ImageProcessor processor) {
310        return processor != null && imageProcessors.add(processor);
311    }
312
313    /**
314     * This method removes given {@link ImageProcessor} from this layer
315     *
316     * @param processor which is needed to be removed
317     *
318     * @return true if processor was removed
319     */
320    public boolean removeImageProcessor(ImageProcessor processor) {
321        return imageProcessors.remove(processor);
322    }
323
324    /**
325     * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
326     * @param op the {@link BufferedImageOp}
327     * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
328     *                (the {@code op} needs to support this!)
329     * @return the {@link ImageProcessor} wrapper
330     */
331    public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
332        return image -> op.filter(image, inPlace ? image : null);
333    }
334
335    /**
336     * This method gets all {@link ImageProcessor}s of the layer
337     *
338     * @return list of image processors without removed one
339     */
340    public List<ImageProcessor> getImageProcessors() {
341        return imageProcessors;
342    }
343
344    /**
345     * Applies all the chosen {@link ImageProcessor}s to the image
346     *
347     * @param img - image which should be changed
348     *
349     * @return the new changed image
350     */
351    public BufferedImage applyImageProcessors(BufferedImage img) {
352        for (ImageProcessor processor : imageProcessors) {
353            img = processor.process(img);
354        }
355        return img;
356    }
357
358    /**
359     * An additional menu entry in the imagery offset menu.
360     * @author Michael Zangl
361     * @see ImageryLayer#getOffsetMenuEntries()
362     * @since 13243
363     */
364    public interface OffsetMenuEntry {
365        /**
366         * Get the label to use for this menu item
367         * @return The label to display in the menu.
368         */
369        String getLabel();
370
371        /**
372         * Test whether this bookmark is currently active
373         * @return <code>true</code> if it is active
374         */
375        boolean isActive();
376
377        /**
378         * Load this bookmark
379         */
380        void actionPerformed();
381    }
382
383    @Override
384    public String toString() {
385        return getClass().getSimpleName() + " [info=" + info + ']';
386    }
387}