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