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