source: josm/trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java

Last change on this file was 19289, checked in by taylor.smock, 10 months ago

Fix #24097: Zoom to imagery layer

This fixes two issues:

  1. Adds implementation for visitBoundingBox used by the Zoom to layer action
  2. Uses addLayer(Layer, boolean) to avoid zooming to the bounds of the layer on layer add

Also, clean up some deprecation warnings.

  • Property svn:eol-style set to native
File size: 14.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Dimension;
8import java.awt.GraphicsEnvironment;
9import java.awt.GridBagConstraints;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.io.IOException;
13import java.net.MalformedURLException;
14import java.nio.file.InvalidPathException;
15import java.time.Year;
16import java.time.ZoneOffset;
17import java.util.Collection;
18import java.util.Collections;
19import java.util.List;
20import java.util.function.Function;
21import java.util.stream.Collectors;
22
23import javax.swing.JCheckBox;
24import javax.swing.JComboBox;
25import javax.swing.JOptionPane;
26import javax.swing.JPanel;
27import javax.swing.JScrollPane;
28
29import org.openstreetmap.josm.data.coor.LatLon;
30import org.openstreetmap.josm.data.imagery.DefaultLayer;
31import org.openstreetmap.josm.data.imagery.ImageryInfo;
32import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
33import org.openstreetmap.josm.data.imagery.LayerDetails;
34import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
35import org.openstreetmap.josm.data.imagery.WMTSTileSource;
36import org.openstreetmap.josm.data.imagery.WMTSTileSource.Layer;
37import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
38import org.openstreetmap.josm.data.projection.ProjectionRegistry;
39import org.openstreetmap.josm.gui.ExtendedDialog;
40import org.openstreetmap.josm.gui.MainApplication;
41import org.openstreetmap.josm.gui.layer.AlignImageryPanel;
42import org.openstreetmap.josm.gui.layer.ImageryLayer;
43import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
44import org.openstreetmap.josm.gui.preferences.imagery.WMSLayerTree;
45import org.openstreetmap.josm.gui.util.GuiHelper;
46import org.openstreetmap.josm.io.imagery.WMSImagery;
47import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
48import org.openstreetmap.josm.tools.CheckParameterUtil;
49import org.openstreetmap.josm.tools.GBC;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.Logging;
52import org.openstreetmap.josm.tools.Utils;
53import org.openstreetmap.josm.tools.bugreport.ReportedException;
54
55/**
56 * Action displayed in imagery menu to add a new imagery layer.
57 * @since 3715
58 */
59public class AddImageryLayerAction extends JosmAction implements AdaptableAction {
60 private final transient ImageryInfo info;
61
62 static class SelectWmsLayersDialog extends ExtendedDialog {
63 SelectWmsLayersDialog(WMSLayerTree tree, JComboBox<String> formats) {
64 super(MainApplication.getMainFrame(), tr("Select WMS layers"), tr("Add layers"), tr("Cancel"));
65 final JScrollPane scrollPane = new JScrollPane(tree.getLayerTree());
66 scrollPane.setPreferredSize(new Dimension(400, 400));
67 final JPanel panel = new JPanel(new GridBagLayout());
68 panel.add(scrollPane, GBC.eol().fill());
69 panel.add(formats, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
70 setContent(panel);
71 }
72 }
73
74 /**
75 * Constructs a new {@code AddImageryLayerAction} for the given {@code ImageryInfo}.
76 * If an http:// icon is specified, it is fetched asynchronously.
77 * @param info The imagery info
78 */
79 public AddImageryLayerAction(ImageryInfo info) {
80 super(info.getMenuName(), /* ICON */"imagery_menu", info.getToolTipText(), null,
81 true, ToolbarPreferences.IMAGERY_PREFIX + info.getToolbarName(), false);
82 setHelpId(ht("/Preferences/Imagery"));
83 this.info = info;
84 installAdapters();
85
86 // change toolbar icon from if specified
87 String icon = info.getIcon();
88 if (icon != null) {
89 new ImageProvider(icon).setOptional(true).getResourceAsync(result -> {
90 if (result != null) {
91 GuiHelper.runInEDT(() -> result.attachImageIcon(this));
92 }
93 });
94 }
95 }
96
97 /**
98 * Converts general ImageryInfo to specific one, that does not need any user action to initialize
99 * see: https://josm.openstreetmap.de/ticket/13868
100 * @param info ImageryInfo that will be converted (or returned when no conversion needed)
101 * @return ImageryInfo object that's ready to be used to create TileSource
102 */
103 private static ImageryInfo convertImagery(ImageryInfo info) {
104 try {
105 if (info.getUrl() != null && info.getUrl().contains("{time}")) {
106 final String instant = Year.now(ZoneOffset.UTC).atDay(1).atStartOfDay(ZoneOffset.UTC).toInstant().toString();
107 final String example = String.join("/", instant, instant);
108 final String initialSelectionValue = info.getDate() != null ? info.getDate() : example;
109 final String userDate = JOptionPane.showInputDialog(MainApplication.getMainFrame(),
110 tr("Time filter for \"{0}\" such as \"{1}\"", info.getName(), example),
111 initialSelectionValue);
112 if (userDate == null) {
113 return null;
114 }
115 info.setDate(userDate);
116 // TODO persist new {time} value (via ImageryLayerInfo.save?)
117 }
118 switch (info.getImageryType()) {
119 case WMS_ENDPOINT:
120 // convert to WMS type
121 if (Utils.isEmpty(info.getDefaultLayers())) {
122 return getWMSLayerInfo(info);
123 } else {
124 return info;
125 }
126 case WMTS:
127 // specify which layer to use
128 if (Utils.isEmpty(info.getDefaultLayers())) {
129 WMTSTileSource tileSource = new WMTSTileSource(info);
130 DefaultLayer layerId = tileSource.userSelectLayer();
131 if (layerId != null) {
132 ImageryInfo copy = new ImageryInfo(info);
133 copy.setDefaultLayers(Collections.singletonList(layerId));
134 String layerName = tileSource.getLayers().stream()
135 .filter(x -> x.getIdentifier().equals(layerId.getLayerName()))
136 .map(Layer::getUserTitle)
137 .findFirst()
138 .orElse("");
139 copy.setName(copy.getName() + ": " + layerName);
140 return copy;
141 }
142 return null;
143 } else {
144 return info;
145 }
146 default:
147 return info;
148 }
149 } catch (MalformedURLException ex) {
150 handleException(ex, tr("Invalid service URL."), tr("WMS Error"), null);
151 } catch (IOException ex) {
152 handleException(ex, tr("Could not retrieve WMS layer list."), tr("WMS Error"), null);
153 } catch (WMSGetCapabilitiesException ex) {
154 handleException(ex, tr("Could not parse WMS layer list."), tr("WMS Error"),
155 "Could not parse WMS layer list. Incoming data:\n" + ex.getIncomingData());
156 } catch (WMTSGetCapabilitiesException ex) {
157 handleException(ex, tr("Could not parse WMTS layer list."), tr("WMTS Error"),
158 "Could not parse WMTS layer list.");
159 }
160 return null;
161 }
162
163 @Override
164 public void actionPerformed(ActionEvent e) {
165 if (!isEnabled()) return;
166 ImageryLayer layer = null;
167 try {
168 final ImageryInfo infoToAdd = convertImagery(info);
169 if (infoToAdd != null) {
170 layer = ImageryLayer.create(infoToAdd);
171 getLayerManager().addLayer(layer, false);
172 AlignImageryPanel.addNagPanelIfNeeded(infoToAdd);
173 }
174 } catch (IllegalArgumentException | ReportedException ex) {
175 if (Utils.isEmpty(ex.getMessage()) || GraphicsEnvironment.isHeadless()) {
176 throw ex;
177 } else {
178 Logging.error(ex);
179 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), ex.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
180 if (layer != null) {
181 getLayerManager().removeLayer(layer);
182 }
183 }
184 }
185 }
186
187 /**
188 * Represents the user choices when selecting layers to display.
189 * @since 14549
190 */
191 public static class LayerSelection {
192 private final List<LayerDetails> layers;
193 private final String format;
194 private final boolean transparent;
195
196 /**
197 * Constructs a new {@code LayerSelection}.
198 * @param layers selected layers
199 * @param format selected image format
200 * @param transparent enable transparency?
201 */
202 public LayerSelection(List<LayerDetails> layers, String format, boolean transparent) {
203 this.layers = layers;
204 this.format = format;
205 this.transparent = transparent;
206 }
207 }
208
209 private static LayerSelection askToSelectLayers(WMSImagery wms) {
210 final WMSLayerTree tree = new WMSLayerTree();
211
212 Collection<String> wmsFormats = wms.getFormats();
213 final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[0]));
214 formats.setSelectedItem(wms.getPreferredFormat());
215 formats.setToolTipText(tr("Select image format for WMS layer"));
216
217 JCheckBox checkBounds = new JCheckBox(tr("Show only layers for current view"), true);
218 Runnable updateTree = () -> {
219 LatLon latLon = checkBounds.isSelected() && MainApplication.isDisplayingMapView()
220 ? MainApplication.getMap().mapView.getProjection().eastNorth2latlon(MainApplication.getMap().mapView.getCenter())
221 : null;
222 tree.setCheckBounds(latLon);
223 tree.updateTree(wms);
224 System.out.println(wms);
225 };
226 checkBounds.addActionListener(ignore -> updateTree.run());
227 updateTree.run();
228
229 if (!GraphicsEnvironment.isHeadless()) {
230 ExtendedDialog dialog = new ExtendedDialog(MainApplication.getMainFrame(),
231 tr("Select WMS layers"), tr("Add layers"), tr("Cancel"));
232 final JScrollPane scrollPane = new JScrollPane(tree.getLayerTree());
233 scrollPane.setPreferredSize(new Dimension(400, 400));
234 final JPanel panel = new JPanel(new GridBagLayout());
235 panel.add(scrollPane, GBC.eol().fill());
236 panel.add(checkBounds, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
237 panel.add(formats, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
238 dialog.setContent(panel);
239
240 if (dialog.showDialog().getValue() != 1) {
241 return null;
242 }
243 }
244 return new LayerSelection(
245 tree.getSelectedLayers(),
246 (String) formats.getSelectedItem(),
247 true); // TODO: ask the user if transparent layer is wanted
248 }
249
250 /**
251 * Asks user to choose a WMS layer from a WMS endpoint.
252 * @param info the WMS endpoint.
253 * @return chosen WMS layer, or null
254 * @throws IOException if any I/O error occurs while contacting the WMS endpoint
255 * @throws WMSGetCapabilitiesException if the WMS getCapabilities request fails
256 * @throws InvalidPathException if a Path object cannot be constructed for the capabilities cached file
257 */
258 protected static ImageryInfo getWMSLayerInfo(ImageryInfo info) throws IOException, WMSGetCapabilitiesException {
259 try {
260 return getWMSLayerInfo(info, AddImageryLayerAction::askToSelectLayers);
261 } catch (MalformedURLException ex) {
262 handleException(ex, tr("Invalid service URL."), tr("WMS Error"), null);
263 } catch (IOException ex) {
264 handleException(ex, tr("Could not retrieve WMS layer list."), tr("WMS Error"), null);
265 } catch (WMSGetCapabilitiesException ex) {
266 handleException(ex, tr("Could not parse WMS layer list."), tr("WMS Error"),
267 "Could not parse WMS layer list. Incoming data:\n" + ex.getIncomingData());
268 }
269 return null;
270 }
271
272 /**
273 * Asks user to choose a WMS layer from a WMS endpoint.
274 * @param info the WMS endpoint.
275 * @param choice how the user may choose the WMS layer
276 * @return chosen WMS layer, or null
277 * @throws IOException if any I/O error occurs while contacting the WMS endpoint
278 * @throws WMSGetCapabilitiesException if the WMS getCapabilities request fails
279 * @throws InvalidPathException if a Path object cannot be constructed for the capabilities cached file
280 * @since 14549
281 */
282 public static ImageryInfo getWMSLayerInfo(ImageryInfo info, Function<WMSImagery, LayerSelection> choice)
283 throws IOException, WMSGetCapabilitiesException {
284 CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT == info.getImageryType(), "wms_endpoint imagery type expected");
285 // We need to get the URL with {apikey} replaced. See #22642.
286 final TemplatedWMSTileSource tileSource = new TemplatedWMSTileSource(info, ProjectionRegistry.getProjection());
287 final WMSImagery wms = new WMSImagery(tileSource.getBaseUrl(), info.getCustomHttpHeaders());
288 LayerSelection selection = choice.apply(wms);
289 if (selection == null) {
290 return null;
291 }
292
293 final String url = wms.buildGetMapUrl(
294 selection.layers.stream().map(LayerDetails::getName).collect(Collectors.toList()),
295 (List<String>) null,
296 selection.format,
297 selection.transparent
298 );
299
300 String selectedLayers = selection.layers.stream()
301 .map(LayerDetails::getName)
302 .collect(Collectors.joining(", "));
303 // Use full copy of original Imagery info to copy all attributes. Only overwrite what's different
304 ImageryInfo ret = new ImageryInfo(info);
305 ret.setUrl(url);
306 ret.setImageryType(ImageryType.WMS);
307 ret.setName(info.getName() + " - " + selectedLayers);
308 ret.setServerProjections(wms.getServerProjections(selection.layers));
309 return ret;
310 }
311
312 private static void handleException(Exception ex, String uiMessage, String uiTitle, String logMessage) {
313 if (!GraphicsEnvironment.isHeadless()) {
314 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), uiMessage, uiTitle, JOptionPane.ERROR_MESSAGE);
315 }
316 Logging.log(Logging.LEVEL_ERROR, logMessage, ex);
317 }
318
319 @Override
320 protected boolean listenToSelectionChange() {
321 return false;
322 }
323
324 @Override
325 protected void updateEnabledState() {
326 setEnabled(!info.isBlacklisted());
327 }
328
329 @Override
330 public String toString() {
331 return "AddImageryLayerAction [info=" + info + ']';
332 }
333}
Note: See TracBrowser for help on using the repository browser.