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}