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