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