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}