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