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