001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Dimension;
008import java.awt.GraphicsEnvironment;
009import java.awt.GridBagLayout;
010import java.awt.event.ActionEvent;
011import java.io.IOException;
012import java.net.MalformedURLException;
013import java.util.ArrayList;
014import java.util.Collection;
015import java.util.HashSet;
016import java.util.List;
017import java.util.Set;
018
019import javax.swing.JComboBox;
020import javax.swing.JOptionPane;
021import javax.swing.JPanel;
022import javax.swing.JScrollPane;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.data.imagery.DefaultLayer;
026import org.openstreetmap.josm.data.imagery.ImageryInfo;
027import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
028import org.openstreetmap.josm.data.imagery.WMTSTileSource;
029import org.openstreetmap.josm.gui.ExtendedDialog;
030import org.openstreetmap.josm.gui.layer.AlignImageryPanel;
031import org.openstreetmap.josm.gui.layer.ImageryLayer;
032import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
033import org.openstreetmap.josm.gui.preferences.imagery.WMSLayerTree;
034import org.openstreetmap.josm.gui.util.GuiHelper;
035import org.openstreetmap.josm.io.imagery.WMSImagery;
036import org.openstreetmap.josm.io.imagery.WMSImagery.LayerDetails;
037import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
038import org.openstreetmap.josm.tools.CheckParameterUtil;
039import org.openstreetmap.josm.tools.GBC;
040import org.openstreetmap.josm.tools.ImageProvider;
041import org.openstreetmap.josm.tools.Logging;
042import org.openstreetmap.josm.tools.bugreport.ReportedException;
043
044/**
045 * Action displayed in imagery menu to add a new imagery layer.
046 * @since 3715
047 */
048public class AddImageryLayerAction extends JosmAction implements AdaptableAction {
049    private final transient ImageryInfo info;
050
051    static class SelectWmsLayersDialog extends ExtendedDialog {
052        SelectWmsLayersDialog(WMSLayerTree tree, JComboBox<String> formats) {
053            super(Main.parent, tr("Select WMS layers"), tr("Add layers"), tr("Cancel"));
054            final JScrollPane scrollPane = new JScrollPane(tree.getLayerTree());
055            scrollPane.setPreferredSize(new Dimension(400, 400));
056            final JPanel panel = new JPanel(new GridBagLayout());
057            panel.add(scrollPane, GBC.eol().fill());
058            panel.add(formats, GBC.eol().fill(GBC.HORIZONTAL));
059            setContent(panel);
060        }
061    }
062
063    /**
064     * Constructs a new {@code AddImageryLayerAction} for the given {@code ImageryInfo}.
065     * If an http:// icon is specified, it is fetched asynchronously.
066     * @param info The imagery info
067     */
068    public AddImageryLayerAction(ImageryInfo info) {
069        super(info.getMenuName(), /* ICON */"imagery_menu", tr("Add imagery layer {0}", info.getName()), null,
070                true, ToolbarPreferences.IMAGERY_PREFIX + info.getToolbarName(), false);
071        putValue("help", ht("/Preferences/Imagery"));
072        setTooltip(info.getToolTipText().replaceAll("</?html>", ""));
073        this.info = info;
074        installAdapters();
075
076        // change toolbar icon from if specified
077        String icon = info.getIcon();
078        if (icon != null) {
079            new ImageProvider(icon).setOptional(true).getResourceAsync(result -> {
080                if (result != null) {
081                    GuiHelper.runInEDT(() -> result.attachImageIcon(this));
082                }
083            });
084        }
085    }
086
087    /**
088     * Converts general ImageryInfo to specific one, that does not need any user action to initialize
089     * see: https://josm.openstreetmap.de/ticket/13868
090     * @param info ImageryInfo that will be converted (or returned when no conversion needed)
091     * @return ImageryInfo object that's ready to be used to create TileSource
092     */
093    private ImageryInfo convertImagery(ImageryInfo info) {
094        try {
095            switch(info.getImageryType()) {
096            case WMS_ENDPOINT:
097                // convert to WMS type
098                return getWMSLayerInfo(info);
099            case WMTS:
100                // specify which layer to use
101                DefaultLayer layerId = new WMTSTileSource(info).userSelectLayer();
102                if (layerId != null) {
103                    ImageryInfo copy = new ImageryInfo(info);
104                    Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
105                    defaultLayers.add(layerId);
106                    copy.setDefaultLayers(defaultLayers);
107                    return copy;
108                }
109                // layer not selected - refuse to add
110                return null;
111            default:
112                return info;
113            }
114        } catch (MalformedURLException ex) {
115            if (!GraphicsEnvironment.isHeadless()) {
116                JOptionPane.showMessageDialog(Main.parent, tr("Invalid service URL."),
117                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
118            }
119            Logging.log(Logging.LEVEL_ERROR, ex);
120        } catch (IOException ex) {
121            if (!GraphicsEnvironment.isHeadless()) {
122                JOptionPane.showMessageDialog(Main.parent, tr("Could not retrieve WMS layer list."),
123                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
124            }
125            Logging.log(Logging.LEVEL_ERROR, ex);
126        } catch (WMSGetCapabilitiesException ex) {
127            if (!GraphicsEnvironment.isHeadless()) {
128                JOptionPane.showMessageDialog(Main.parent, tr("Could not parse WMS layer list."),
129                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
130            }
131            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMS layer list. Incoming data:\n"+ex.getIncomingData(), ex);
132        }
133        return null;
134    }
135
136    @Override
137    public void actionPerformed(ActionEvent e) {
138        if (!isEnabled()) return;
139        ImageryLayer layer = null;
140        try {
141            final ImageryInfo infoToAdd = convertImagery(info);
142            if (infoToAdd != null) {
143                layer = ImageryLayer.create(infoToAdd);
144                getLayerManager().addLayer(layer);
145                AlignImageryPanel.addNagPanelIfNeeded(infoToAdd);
146            }
147        } catch (IllegalArgumentException | ReportedException ex) {
148            if (ex.getMessage() == null || ex.getMessage().isEmpty() || GraphicsEnvironment.isHeadless()) {
149                throw ex;
150            } else {
151                Logging.error(ex);
152                JOptionPane.showMessageDialog(Main.parent, ex.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
153                if (layer != null) {
154                    getLayerManager().removeLayer(layer);
155                }
156            }
157        }
158    }
159
160    /**
161     * Asks user to choose a WMS layer from a WMS endpoint.
162     * @param info the WMS endpoint.
163     * @return chosen WMS layer, or null
164     * @throws IOException if any I/O error occurs while contacting the WMS endpoint
165     * @throws WMSGetCapabilitiesException if the WMS getCapabilities request fails
166     */
167    protected static ImageryInfo getWMSLayerInfo(ImageryInfo info) throws IOException, WMSGetCapabilitiesException {
168        CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT.equals(info.getImageryType()), "wms_endpoint imagery type expected");
169
170        final WMSImagery wms = new WMSImagery();
171        wms.attemptGetCapabilities(info.getUrl());
172
173        final WMSLayerTree tree = new WMSLayerTree();
174        tree.updateTree(wms);
175        List<String> wmsFormats = wms.getFormats();
176        final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[0]));
177        formats.setSelectedItem(wms.getPreferredFormats());
178        formats.setToolTipText(tr("Select image format for WMS layer"));
179
180        if (!GraphicsEnvironment.isHeadless() && 1 != new SelectWmsLayersDialog(tree, formats).showDialog().getValue()) {
181            return null;
182        }
183
184        final String url = wms.buildGetMapUrl(
185                tree.getSelectedLayers(), (String) formats.getSelectedItem());
186        Set<String> supportedCrs = new HashSet<>();
187        boolean first = true;
188        StringBuilder layersString = new StringBuilder();
189        for (LayerDetails layer: tree.getSelectedLayers()) {
190            if (first) {
191                supportedCrs.addAll(layer.getProjections());
192                first = false;
193            }
194            layersString.append(layer.name);
195            layersString.append(", ");
196            supportedCrs.retainAll(layer.getProjections());
197        }
198
199        // copy all information from WMS
200        ImageryInfo ret = new ImageryInfo(info);
201        // and update according to user choice
202        ret.setUrl(url);
203        ret.setImageryType(ImageryType.WMS);
204        if (layersString.length() > 2) {
205            ret.setName(ret.getName() + ' ' + layersString.substring(0, layersString.length() - 2));
206        }
207        ret.setServerProjections(supportedCrs);
208        return ret;
209    }
210
211    @Override
212    protected void updateEnabledState() {
213        if (info.isBlacklisted()) {
214            setEnabled(false);
215        } else {
216            setEnabled(true);
217        }
218    }
219
220    @Override
221    public String toString() {
222        return "AddImageryLayerAction [info=" + info + ']';
223    }
224}