source: josm/trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java

Last change on this file was 19200, checked in by taylor.smock, 11 months ago

Fix #23821: Ensure that remote control commands are processed in order

This reverts or partially reverts r19153 and r19196 in favour of forcing ordering
in the RequestProcessor#run method. This does not block the server thread, but
it can mean that we have a bunch of processor threads that are waiting on the
previous processor thread.

  • Property svn:eol-style set to native
File size: 19.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.remotecontrol.handler;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.geom.Area;
7import java.awt.geom.Rectangle2D;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.LinkedHashSet;
11import java.util.List;
12import java.util.Map;
13import java.util.Set;
14import java.util.concurrent.ExecutionException;
15import java.util.concurrent.Future;
16import java.util.concurrent.TimeUnit;
17import java.util.concurrent.TimeoutException;
18import java.util.stream.Collectors;
19import java.util.stream.Stream;
20
21import javax.swing.JOptionPane;
22
23import org.openstreetmap.josm.actions.AutoScaleAction;
24import org.openstreetmap.josm.actions.AutoScaleAction.AutoScaleMode;
25import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
26import org.openstreetmap.josm.actions.downloadtasks.DownloadParams;
27import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
28import org.openstreetmap.josm.data.Bounds;
29import org.openstreetmap.josm.data.coor.LatLon;
30import org.openstreetmap.josm.data.osm.BBox;
31import org.openstreetmap.josm.data.osm.DataSet;
32import org.openstreetmap.josm.data.osm.OsmPrimitive;
33import org.openstreetmap.josm.data.osm.Relation;
34import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
35import org.openstreetmap.josm.data.osm.search.SearchCompiler;
36import org.openstreetmap.josm.data.osm.search.SearchParseError;
37import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
38import org.openstreetmap.josm.gui.ExceptionDialogUtil;
39import org.openstreetmap.josm.gui.MainApplication;
40import org.openstreetmap.josm.gui.MapFrame;
41import org.openstreetmap.josm.gui.Notification;
42import org.openstreetmap.josm.gui.util.GuiHelper;
43import org.openstreetmap.josm.io.OsmApiException;
44import org.openstreetmap.josm.io.OsmTransferException;
45import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog;
46import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
47import org.openstreetmap.josm.tools.Logging;
48import org.openstreetmap.josm.tools.SubclassFilteredCollection;
49import org.openstreetmap.josm.tools.Utils;
50
51/**
52 * Handler for {@code load_and_zoom} and {@code zoom} requests.
53 * @since 3707
54 */
55public class LoadAndZoomHandler extends RequestHandler {
56
57 /**
58 * The remote control command name used to load data and zoom.
59 */
60 public static final String command = "load_and_zoom";
61
62 /**
63 * The remote control command name used to zoom.
64 */
65 public static final String command2 = "zoom";
66 private static final String CURRENT_SELECTION = "currentselection";
67 private static final String SELECT = "select";
68 private static final String ADDTAGS = "addtags";
69 private static final String CHANGESET_COMMENT = "changeset_comment";
70 private static final String CHANGESET_SOURCE = "changeset_source";
71 private static final String CHANGESET_HASHTAGS = "changeset_hashtags";
72 private static final String CHANGESET_TAGS = "changeset_tags";
73 private static final String SEARCH = "search";
74
75 // Mandatory arguments
76 private double minlat;
77 private double maxlat;
78 private double minlon;
79 private double maxlon;
80
81 // Optional argument 'select'
82 private final Set<SimplePrimitiveId> toSelect = new LinkedHashSet<>();
83
84 private boolean isKeepingCurrentSelection;
85
86 @Override
87 public String getPermissionMessage() {
88 String msg = tr("Remote Control has been asked to load data from the API.") +
89 "<br>" + tr("Bounding box: ") + new BBox(minlon, minlat, maxlon, maxlat).toStringCSV(", ");
90 if (args.containsKey(SELECT) && !toSelect.isEmpty()) {
91 msg += "<br>" + tr("Selection: {0}", toSelect.size());
92 }
93 return msg;
94 }
95
96 @Override
97 public String[] getMandatoryParams() {
98 return new String[] {"bottom", "top", "left", "right"};
99 }
100
101 @Override
102 public String[] getOptionalParams() {
103 return new String[] {"new_layer", "layer_name", ADDTAGS, SELECT, "zoom_mode",
104 CHANGESET_COMMENT, CHANGESET_SOURCE, CHANGESET_HASHTAGS, CHANGESET_TAGS,
105 SEARCH, "layer_locked", "download_policy", "upload_policy"};
106 }
107
108 @Override
109 public String getUsage() {
110 return "download a bounding box from the API, zoom to the downloaded area and optionally select one or more objects";
111 }
112
113 @Override
114 public String[] getUsageExamples() {
115 return getUsageExamples(myCommand);
116 }
117
118 @Override
119 public String[] getUsageExamples(String cmd) {
120 if (command.equals(cmd)) {
121 return new String[] {
122 "/load_and_zoom?addtags=wikipedia:de=Wei%C3%9Fe_Gasse|maxspeed=5&select=way23071688,way23076176,way23076177," +
123 "&left=13.740&right=13.741&top=51.05&bottom=51.049",
124 "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"};
125 } else {
126 return new String[] {
127 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999",
128 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=highway+OR+railway",
129 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=" + CURRENT_SELECTION + "&addtags=foo=bar",
130 };
131 }
132 }
133
134 @Override
135 protected void handleRequest() throws RequestHandlerErrorException {
136 download();
137 /*
138 * deselect objects if parameter addtags given
139 */
140 if (args.containsKey(ADDTAGS) && !isKeepingCurrentSelection) {
141 GuiHelper.executeByMainWorkerInEDT(() -> {
142 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
143 if (ds == null) // e.g. download failed
144 return;
145 ds.clearSelection();
146 });
147 }
148
149 Collection<OsmPrimitive> forTagAdd = performSearchZoom();
150
151 // This comes before the other changeset tags, so that they can be overridden
152 parseChangesetTags(args);
153
154 // add changeset tags after download if necessary
155 addChangesetTags();
156
157 // add tags to objects
158 addTags(forTagAdd);
159 }
160
161 private void download() throws RequestHandlerErrorException {
162 DownloadOsmTask osmTask = new DownloadOsmTask();
163 try {
164 final DownloadParams settings = getDownloadParams();
165
166 if (command.equals(myCommand)) {
167 if (!PermissionPrefWithDefault.LOAD_DATA.isAllowed()) {
168 Logging.info("RemoteControl: download forbidden by preferences");
169 } else {
170 Area toDownload = null;
171 if (!settings.isNewLayer()) {
172 toDownload = removeAlreadyDownloadedArea();
173 }
174 if (toDownload != null && toDownload.isEmpty()) {
175 Logging.info("RemoteControl: no download necessary");
176 } else {
177 performDownload(osmTask, settings);
178 }
179 }
180 }
181 } catch (RuntimeException ex) { // NOPMD
182 Logging.warn("RemoteControl: Error parsing load_and_zoom remote control request:");
183 Logging.error(ex);
184 throw new RequestHandlerErrorException(ex);
185 }
186 }
187
188 /**
189 * Remove areas that has already been downloaded
190 * @return The area to download
191 */
192 private Area removeAlreadyDownloadedArea() {
193 // find out whether some data has already been downloaded
194 Area toDownload = null;
195 Area present = null;
196 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
197 if (ds != null) {
198 present = ds.getDataSourceArea();
199 }
200 if (present != null && !present.isEmpty()) {
201 toDownload = new Area(new Rectangle2D.Double(minlon, minlat, maxlon-minlon, maxlat-minlat));
202 toDownload.subtract(present);
203 if (!toDownload.isEmpty()) {
204 // the result might not be a rectangle (L shaped etc)
205 Rectangle2D downloadBounds = toDownload.getBounds2D();
206 minlat = downloadBounds.getMinY();
207 minlon = downloadBounds.getMinX();
208 maxlat = downloadBounds.getMaxY();
209 maxlon = downloadBounds.getMaxX();
210 }
211 }
212 return toDownload;
213 }
214
215 /**
216 * Perform the actual download; this is synchronized to ensure that we only have one download going on at a time
217 * @param osmTask The task that will show a dialog
218 * @param settings The download settings
219 * @throws RequestHandlerErrorException If there is an issue getting data
220 */
221 private void performDownload(DownloadOsmTask osmTask, DownloadParams settings) throws RequestHandlerErrorException {
222 Future<?> future = MainApplication.worker.submit(
223 new PostDownloadHandler(osmTask, osmTask.download(settings, new Bounds(minlat, minlon, maxlat, maxlon),
224 null /* let the task manage the progress monitor */)));
225 GuiHelper.executeByMainWorkerInEDT(() -> {
226 try {
227 future.get(OSM_DOWNLOAD_TIMEOUT.get(), TimeUnit.SECONDS);
228 if (osmTask.isFailed()) {
229 Object error = osmTask.getErrorObjects().get(0);
230 if (error instanceof OsmApiException) {
231 throw (OsmApiException) error;
232 }
233 List<Throwable> exceptions = osmTask.getErrorObjects().stream()
234 .filter(Throwable.class::isInstance).map(Throwable.class::cast)
235 .collect(Collectors.toList());
236 OsmTransferException osmTransferException =
237 new OsmTransferException(String.join(", ", osmTask.getErrorMessages()));
238 if (!exceptions.isEmpty()) {
239 osmTransferException.initCause(exceptions.get(0));
240 exceptions.remove(0);
241 exceptions.forEach(osmTransferException::addSuppressed);
242 }
243 throw osmTransferException;
244 }
245 } catch (InterruptedException ex) {
246 Thread.currentThread().interrupt();
247 ExceptionDialogUtil.explainException(ex);
248 } catch (ExecutionException | TimeoutException |
249 OsmTransferException | RuntimeException ex) { // NOPMD
250 ExceptionDialogUtil.explainException(ex);
251 }
252 });
253 // Don't block forever, but do wait some period of time.
254 try {
255 future.get(OSM_DOWNLOAD_TIMEOUT.get(), TimeUnit.SECONDS);
256 } catch (InterruptedException e) {
257 Thread.currentThread().interrupt();
258 throw new RequestHandlerErrorException(e);
259 } catch (TimeoutException | ExecutionException e) {
260 throw new RequestHandlerErrorException(e);
261 }
262 }
263
264 private Collection<OsmPrimitive> performSearchZoom() throws RequestHandlerErrorException {
265 final Collection<OsmPrimitive> forTagAdd = new LinkedHashSet<>();
266 final Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon);
267 if (args.containsKey(SELECT) && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
268 // select objects after downloading, zoom to selection.
269 GuiHelper.executeByMainWorkerInEDT(() -> selectAndZoom(forTagAdd, bbox));
270 } else if (args.containsKey(SEARCH) && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
271 searchAndZoom(forTagAdd, bbox);
272 } else {
273 // after downloading, zoom to downloaded area.
274 zoom(Collections.emptySet(), bbox);
275 }
276 return forTagAdd;
277 }
278
279 private void selectAndZoom(Collection<OsmPrimitive> forTagAdd, Bounds bbox) {
280 Set<OsmPrimitive> newSel = new LinkedHashSet<>();
281 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
282 if (ds == null) // e.g. download failed
283 return;
284 for (SimplePrimitiveId id : toSelect) {
285 final OsmPrimitive p = ds.getPrimitiveById(id);
286 if (p != null) {
287 newSel.add(p);
288 forTagAdd.add(p);
289 }
290 }
291 if (isKeepingCurrentSelection) {
292 Collection<OsmPrimitive> sel = ds.getSelected();
293 newSel.addAll(sel);
294 forTagAdd.addAll(sel);
295 }
296 toSelect.clear();
297 ds.setSelected(newSel);
298 zoom(newSel, bbox);
299 MapFrame map = MainApplication.getMap();
300 if (MainApplication.isDisplayingMapView() && map.relationListDialog != null) {
301 map.relationListDialog.selectRelations(null); // unselect all relations to fix #7342
302 map.relationListDialog.dataChanged(null);
303 map.relationListDialog.selectRelations(Utils.filteredCollection(newSel, Relation.class));
304 }
305 }
306
307 private void searchAndZoom(Collection<OsmPrimitive> forTagAdd, Bounds bbox) throws RequestHandlerErrorException {
308 try {
309 final SearchCompiler.Match search = SearchCompiler.compile(args.get(SEARCH));
310 MainApplication.worker.submit(() -> {
311 final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
312 final Collection<OsmPrimitive> filteredPrimitives = SubclassFilteredCollection.filter(ds.allPrimitives(), search);
313 ds.setSelected(filteredPrimitives);
314 forTagAdd.addAll(filteredPrimitives);
315 zoom(filteredPrimitives, bbox);
316 });
317 } catch (SearchParseError ex) {
318 Logging.error(ex);
319 throw new RequestHandlerErrorException(ex);
320 }
321 }
322
323 private void addChangesetTags() {
324 List<String> values = Stream.of(CHANGESET_COMMENT, CHANGESET_SOURCE, CHANGESET_HASHTAGS)
325 .filter(args::containsKey).collect(Collectors.toList());
326 if (values.isEmpty()) {
327 return;
328 }
329 MainApplication.worker.submit(() -> {
330 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
331 if (ds != null) {
332 for (String tag : values) {
333 final String tagKey = tag.substring("changeset_".length());
334 final String value = args.get(tag);
335 if (!Utils.isStripEmpty(value)) {
336 ds.addChangeSetTag(tagKey, value);
337 } else {
338 ds.addChangeSetTag(tagKey, null);
339 }
340 }
341 }
342 });
343 }
344
345 private void addTags(Collection<OsmPrimitive> forTagAdd) {
346 if (args.containsKey(ADDTAGS)) {
347 // needs to run in EDT since forTagAdd is updated in EDT as well
348 GuiHelper.executeByMainWorkerInEDT(() -> {
349 if (!forTagAdd.isEmpty()) {
350 AddTagsDialog.addTags(args, sender, forTagAdd);
351 } else {
352 new Notification(isKeepingCurrentSelection
353 ? tr("You clicked on a JOSM remotecontrol link that would apply tags onto selected objects.\n"
354 + "Since no objects have been selected before this click, no tags were added.\n"
355 + "Select one or more objects and click the link again.")
356 : tr("You clicked on a JOSM remotecontrol link that would apply tags onto objects.\n"
357 + "Unfortunately that link seems to be broken.\n"
358 + "Technical explanation: the URL query parameter ''select='' or ''search='' has an invalid value.\n"
359 + "Ask someone at the origin of the clicked link to fix this.")
360 ).setIcon(JOptionPane.WARNING_MESSAGE).setDuration(Notification.TIME_LONG).show();
361 }
362 });
363 }
364 }
365
366 static void parseChangesetTags(Map<String, String> args) {
367 if (args.containsKey(CHANGESET_TAGS)) {
368 MainApplication.worker.submit(() -> {
369 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
370 if (ds != null) {
371 AddTagsDialog.parseUrlTagsToKeyValues(args.get(CHANGESET_TAGS)).forEach(ds::addChangeSetTag);
372 }
373 });
374 }
375 }
376
377 protected void zoom(Collection<OsmPrimitive> primitives, final Bounds bbox) {
378 if (!PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) {
379 return;
380 }
381 // zoom_mode=(download|selection), defaults to selection
382 if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) {
383 AutoScaleAction.autoScale(AutoScaleMode.SELECTION);
384 } else if (MainApplication.isDisplayingMapView()) {
385 // make sure this isn't called unless there *is* a MapView
386 GuiHelper.executeByMainWorkerInEDT(() -> {
387 BoundingXYVisitor bbox1 = new BoundingXYVisitor();
388 bbox1.visit(bbox);
389 MainApplication.getMap().mapView.zoomTo(bbox1);
390 });
391 }
392 }
393
394 @Override
395 public PermissionPrefWithDefault getPermissionPref() {
396 return null;
397 }
398
399 @Override
400 protected void validateRequest() throws RequestHandlerBadRequestException {
401 validateDownloadParams();
402 // Process mandatory arguments
403 minlat = 0;
404 maxlat = 0;
405 minlon = 0;
406 maxlon = 0;
407 try {
408 minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("bottom") : ""));
409 maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("top") : ""));
410 minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("left") : ""));
411 maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("right") : ""));
412 } catch (NumberFormatException e) {
413 throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+')', e);
414 }
415
416 // Current API 0.6 check: "The latitudes must be between -90 and 90"
417 if (!LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat)) {
418 throw new RequestHandlerBadRequestException(tr("The latitudes must be between {0} and {1}", -90d, 90d));
419 }
420 // Current API 0.6 check: "longitudes between -180 and 180"
421 if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)) {
422 throw new RequestHandlerBadRequestException(tr("The longitudes must be between {0} and {1}", -180d, 180d));
423 }
424 // Current API 0.6 check: "the minima must be less than the maxima"
425 if (minlat > maxlat || minlon > maxlon) {
426 throw new RequestHandlerBadRequestException(tr("The minima must be less than the maxima"));
427 }
428
429 // Process optional argument 'select'
430 validateSelect();
431 }
432
433 private void validateSelect() {
434 if (args != null && args.containsKey(SELECT)) {
435 toSelect.clear();
436 for (String item : args.get(SELECT).split(",", -1)) {
437 if (!item.isEmpty()) {
438 if (CURRENT_SELECTION.equalsIgnoreCase(item)) {
439 isKeepingCurrentSelection = true;
440 continue;
441 }
442 try {
443 toSelect.add(SimplePrimitiveId.fromString(item));
444 } catch (IllegalArgumentException ex) {
445 Logging.log(Logging.LEVEL_WARN, "RemoteControl: invalid selection '" + item + "' ignored", ex);
446 }
447 }
448 }
449 }
450 }
451}
Note: See TracBrowser for help on using the repository browser.