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.preferences.IntegerProperty;
035import org.openstreetmap.josm.gui.MainApplication;
036import org.openstreetmap.josm.gui.MapView;
037import org.openstreetmap.josm.gui.MenuScroller;
038import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
039import org.openstreetmap.josm.gui.widgets.UrlLabel;
040import org.openstreetmap.josm.tools.GBC;
041import org.openstreetmap.josm.tools.ImageProcessor;
042import org.openstreetmap.josm.tools.ImageProvider;
043import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
044
045/**
046 * Abstract base class for background imagery layers ({@link WMSLayer}, {@link TMSLayer}, {@link WMTSLayer}).
047 *
048 * Handles some common tasks, like image filters, image processors, etc.
049 */
050public abstract class ImageryLayer extends Layer {
051
052    /**
053     * The default value for the sharpen filter for each imagery layer.
054     */
055    public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
056
057    private final List<ImageProcessor> imageProcessors = new ArrayList<>();
058
059    protected final ImageryInfo info;
060
061    protected Icon icon;
062
063    private final ImageryFilterSettings filterSettings = new ImageryFilterSettings();
064
065    /**
066     * Constructs a new {@code ImageryLayer}.
067     * @param info imagery info
068     */
069    public ImageryLayer(ImageryInfo info) {
070        super(info.getName());
071        this.info = info;
072        if (info.getIcon() != null) {
073            icon = new ImageProvider(info.getIcon()).setOptional(true).
074                    setMaxSize(ImageSizes.LAYER).get();
075        }
076        if (icon == null) {
077            icon = ImageProvider.get("imagery_small");
078        }
079        for (ImageProcessor processor : filterSettings.getProcessors()) {
080            addImageProcessor(processor);
081        }
082        filterSettings.setSharpenLevel(1 + PROP_SHARPEN_LEVEL.get() / 2f);
083    }
084
085    public double getPPD() {
086        if (!MainApplication.isDisplayingMapView())
087            return Main.getProjection().getDefaultZoomInPPD();
088        MapView mapView = MainApplication.getMap().mapView;
089        ProjectionBounds bounds = mapView.getProjectionBounds();
090        return mapView.getWidth() / (bounds.maxEast - bounds.minEast);
091    }
092
093    /**
094     * Returns imagery info.
095     * @return imagery info
096     */
097    public ImageryInfo getInfo() {
098        return info;
099    }
100
101    @Override
102    public Icon getIcon() {
103        return icon;
104    }
105
106    @Override
107    public boolean isMergable(Layer other) {
108        return false;
109    }
110
111    @Override
112    public void mergeFrom(Layer from) {
113    }
114
115    @Override
116    public Object getInfoComponent() {
117        JPanel panel = new JPanel(new GridBagLayout());
118        panel.add(new JLabel(getToolTipText()), GBC.eol());
119        if (info != null) {
120            List<List<String>> content = new ArrayList<>();
121            content.add(Arrays.asList(tr("Name"), info.getName()));
122            content.add(Arrays.asList(tr("Type"), info.getImageryType().getTypeString().toUpperCase(Locale.ENGLISH)));
123            content.add(Arrays.asList(tr("URL"), info.getUrl()));
124            content.add(Arrays.asList(tr("Id"), info.getId() == null ? "-" : info.getId()));
125            if (info.getMinZoom() != 0) {
126                content.add(Arrays.asList(tr("Min. zoom"), Integer.toString(info.getMinZoom())));
127            }
128            if (info.getMaxZoom() != 0) {
129                content.add(Arrays.asList(tr("Max. zoom"), Integer.toString(info.getMaxZoom())));
130            }
131            if (info.getDescription() != null) {
132                content.add(Arrays.asList(tr("Description"), info.getDescription()));
133            }
134            for (List<String> entry: content) {
135                panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
136                panel.add(GBC.glue(5, 0), GBC.std());
137                panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
138            }
139        }
140        return panel;
141    }
142
143    protected JComponent createTextField(String text) {
144        if (text != null && text.matches("https?://.*")) {
145            return new UrlLabel(text);
146        }
147        JTextField ret = new JTextField(text);
148        ret.setEditable(false);
149        ret.setBorder(BorderFactory.createEmptyBorder());
150        return ret;
151    }
152
153    /**
154     * Create a new imagery layer
155     * @param info The imagery info to use as base
156     * @return The created layer
157     */
158    public static ImageryLayer create(ImageryInfo info) {
159        switch(info.getImageryType()) {
160        case WMS:
161        case WMS_ENDPOINT:
162            return new WMSLayer(info);
163        case WMTS:
164            return new WMTSLayer(info);
165        case TMS:
166        case BING:
167        case SCANEX:
168            return new TMSLayer(info);
169        default:
170            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
171        }
172    }
173
174    private static class ApplyOffsetAction extends AbstractAction {
175        private final transient OffsetMenuEntry menuEntry;
176
177        ApplyOffsetAction(OffsetMenuEntry menuEntry) {
178            super(menuEntry.getLabel());
179            this.menuEntry = menuEntry;
180        }
181
182        @Override
183        public void actionPerformed(ActionEvent ev) {
184            menuEntry.actionPerformed();
185            //TODO: Use some form of listeners for this.
186            MainApplication.getMenu().imageryMenu.refreshOffsetMenu();
187        }
188    }
189
190    public class OffsetAction extends AbstractAction implements LayerAction {
191        @Override
192        public void actionPerformed(ActionEvent e) {
193            // Do nothing
194        }
195
196        @Override
197        public Component createMenuComponent() {
198            return getOffsetMenuItem();
199        }
200
201        @Override
202        public boolean supportLayers(List<Layer> layers) {
203            return false;
204        }
205    }
206
207    /**
208     * Create the menu item that should be added to the offset menu.
209     * It may have a sub menu of e.g. bookmarks added to it.
210     * @return The menu item to add to the imagery menu.
211     */
212    public JMenuItem getOffsetMenuItem() {
213        JMenu subMenu = new JMenu(trc("layer", "Offset"));
214        subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
215        return (JMenuItem) getOffsetMenuItem(subMenu);
216    }
217
218    /**
219     * Create the submenu or the menu item to set the offset of the layer.
220     *
221     * If only one menu item for this layer exists, it is returned by this method.
222     *
223     * If there are multiple, this method appends them to the subMenu and then returns the reference to the subMenu.
224     * @param subMenu The subMenu to use
225     * @return A single menu item to adjust the layer or the passed subMenu to which the menu items were appended.
226     */
227    public JComponent getOffsetMenuItem(JComponent subMenu) {
228        JMenuItem adjustMenuItem = new JMenuItem(getAdjustAction());
229        List<OffsetMenuEntry> usableBookmarks = getOffsetMenuEntries();
230        if (usableBookmarks.isEmpty()) {
231            return adjustMenuItem;
232        }
233
234        subMenu.add(adjustMenuItem);
235        subMenu.add(new JSeparator());
236        int menuItemHeight = 0;
237        for (OffsetMenuEntry b : usableBookmarks) {
238            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
239            item.setSelected(b.isActive());
240            subMenu.add(item);
241            menuItemHeight = item.getPreferredSize().height;
242        }
243        if (menuItemHeight > 0) {
244            if (subMenu instanceof JMenu) {
245                MenuScroller.setScrollerFor((JMenu) subMenu);
246            } else if (subMenu instanceof JPopupMenu) {
247                MenuScroller.setScrollerFor((JPopupMenu) subMenu);
248            }
249        }
250        return subMenu;
251    }
252
253    protected abstract Action getAdjustAction();
254
255    protected abstract List<OffsetMenuEntry> getOffsetMenuEntries();
256
257    /**
258     * Gets the settings for the filter that is applied to this layer.
259     * @return The filter settings.
260     * @since 10547
261     */
262    public ImageryFilterSettings getFilterSettings() {
263        return filterSettings;
264    }
265
266    /**
267     * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
268     *
269     * @param processor that processes the image
270     *
271     * @return true if processor was added, false otherwise
272     */
273    public boolean addImageProcessor(ImageProcessor processor) {
274        return processor != null && imageProcessors.add(processor);
275    }
276
277    /**
278     * This method removes given {@link ImageProcessor} from this layer
279     *
280     * @param processor which is needed to be removed
281     *
282     * @return true if processor was removed
283     */
284    public boolean removeImageProcessor(ImageProcessor processor) {
285        return imageProcessors.remove(processor);
286    }
287
288    /**
289     * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
290     * @param op the {@link BufferedImageOp}
291     * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
292     *                (the {@code op} needs to support this!)
293     * @return the {@link ImageProcessor} wrapper
294     */
295    public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
296        return image -> op.filter(image, inPlace ? image : null);
297    }
298
299    /**
300     * This method gets all {@link ImageProcessor}s of the layer
301     *
302     * @return list of image processors without removed one
303     */
304    public List<ImageProcessor> getImageProcessors() {
305        return imageProcessors;
306    }
307
308    /**
309     * Applies all the chosen {@link ImageProcessor}s to the image
310     *
311     * @param img - image which should be changed
312     *
313     * @return the new changed image
314     */
315    public BufferedImage applyImageProcessors(BufferedImage img) {
316        for (ImageProcessor processor : imageProcessors) {
317            img = processor.process(img);
318        }
319        return img;
320    }
321
322    /**
323     * An additional menu entry in the imagery offset menu.
324     * @author Michael Zangl
325     * @see ImageryLayer#getOffsetMenuEntries()
326     * @since 13243
327     */
328    public interface OffsetMenuEntry {
329        /**
330         * Get the label to use for this menu item
331         * @return The label to display in the menu.
332         */
333        String getLabel();
334
335        /**
336         * Test whether this bookmark is currently active
337         * @return <code>true</code> if it is active
338         */
339        boolean isActive();
340
341        /**
342         * Load this bookmark
343         */
344        void actionPerformed();
345    }
346
347    @Override
348    public String toString() {
349        return getClass().getSimpleName() + " [info=" + info + ']';
350    }
351}