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