001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.remotecontrol.handler; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.geom.Area; 007import java.awt.geom.Rectangle2D; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.HashSet; 012import java.util.Set; 013import java.util.concurrent.Future; 014 015import javax.swing.JOptionPane; 016 017import org.openstreetmap.josm.actions.AutoScaleAction; 018import org.openstreetmap.josm.actions.AutoScaleAction.AutoScaleMode; 019import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask; 020import org.openstreetmap.josm.actions.downloadtasks.DownloadParams; 021import org.openstreetmap.josm.actions.downloadtasks.DownloadTask; 022import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 023import org.openstreetmap.josm.data.Bounds; 024import org.openstreetmap.josm.data.coor.LatLon; 025import org.openstreetmap.josm.data.osm.BBox; 026import org.openstreetmap.josm.data.osm.DataSet; 027import org.openstreetmap.josm.data.osm.OsmPrimitive; 028import org.openstreetmap.josm.data.osm.Relation; 029import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 030import org.openstreetmap.josm.data.osm.search.SearchCompiler; 031import org.openstreetmap.josm.data.osm.search.SearchParseError; 032import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 033import org.openstreetmap.josm.gui.MainApplication; 034import org.openstreetmap.josm.gui.MapFrame; 035import org.openstreetmap.josm.gui.Notification; 036import org.openstreetmap.josm.gui.util.GuiHelper; 037import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog; 038import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault; 039import org.openstreetmap.josm.tools.Logging; 040import org.openstreetmap.josm.tools.SubclassFilteredCollection; 041import org.openstreetmap.josm.tools.Utils; 042 043/** 044 * Handler for {@code load_and_zoom} and {@code zoom} requests. 045 * @since 3707 046 */ 047public class LoadAndZoomHandler extends RequestHandler { 048 049 /** 050 * The remote control command name used to load data and zoom. 051 */ 052 public static final String command = "load_and_zoom"; 053 054 /** 055 * The remote control command name used to zoom. 056 */ 057 public static final String command2 = "zoom"; 058 private static final String CURRENT_SELECTION = "currentselection"; 059 060 // Mandatory arguments 061 private double minlat; 062 private double maxlat; 063 private double minlon; 064 private double maxlon; 065 066 // Optional argument 'select' 067 private final Set<SimplePrimitiveId> toSelect = new HashSet<>(); 068 069 private boolean isKeepingCurrentSelection; 070 071 @Override 072 public String getPermissionMessage() { 073 String msg = tr("Remote Control has been asked to load data from the API.") + 074 "<br>" + tr("Bounding box: ") + new BBox(minlon, minlat, maxlon, maxlat).toStringCSV(", "); 075 if (args.containsKey("select") && !toSelect.isEmpty()) { 076 msg += "<br>" + tr("Selection: {0}", toSelect.size()); 077 } 078 return msg; 079 } 080 081 @Override 082 public String[] getMandatoryParams() { 083 return new String[] {"bottom", "top", "left", "right"}; 084 } 085 086 @Override 087 public String[] getOptionalParams() { 088 return new String[] {"new_layer", "layer_name", "addtags", "select", "zoom_mode", 089 "changeset_comment", "changeset_source", "changeset_hashtags", "search", 090 "layer_locked", "download_policy", "upload_policy"}; 091 } 092 093 @Override 094 public String getUsage() { 095 return "download a bounding box from the API, zoom to the downloaded area and optionally select one or more objects"; 096 } 097 098 @Override 099 public String[] getUsageExamples() { 100 return getUsageExamples(myCommand); 101 } 102 103 @Override 104 public String[] getUsageExamples(String cmd) { 105 if (command.equals(cmd)) { 106 return new String[] { 107 "/load_and_zoom?addtags=wikipedia:de=Wei%C3%9Fe_Gasse|maxspeed=5&select=way23071688,way23076176,way23076177," + 108 "&left=13.740&right=13.741&top=51.05&bottom=51.049", 109 "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"}; 110 } else { 111 return new String[] { 112 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999", 113 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=highway+OR+railway", 114 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=" + CURRENT_SELECTION + "&addtags=foo=bar", 115 }; 116 } 117 } 118 119 @Override 120 protected void handleRequest() throws RequestHandlerErrorException { 121 DownloadTask osmTask = new DownloadOsmTask(); 122 try { 123 DownloadParams settings = getDownloadParams(); 124 125 if (command.equals(myCommand)) { 126 if (!PermissionPrefWithDefault.LOAD_DATA.isAllowed()) { 127 Logging.info("RemoteControl: download forbidden by preferences"); 128 } else { 129 Area toDownload = null; 130 if (!settings.isNewLayer()) { 131 // find out whether some data has already been downloaded 132 Area present = null; 133 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 134 if (ds != null) { 135 present = ds.getDataSourceArea(); 136 } 137 if (present != null && !present.isEmpty()) { 138 toDownload = new Area(new Rectangle2D.Double(minlon, minlat, maxlon-minlon, maxlat-minlat)); 139 toDownload.subtract(present); 140 if (!toDownload.isEmpty()) { 141 // the result might not be a rectangle (L shaped etc) 142 Rectangle2D downloadBounds = toDownload.getBounds2D(); 143 minlat = downloadBounds.getMinY(); 144 minlon = downloadBounds.getMinX(); 145 maxlat = downloadBounds.getMaxY(); 146 maxlon = downloadBounds.getMaxX(); 147 } 148 } 149 } 150 if (toDownload != null && toDownload.isEmpty()) { 151 Logging.info("RemoteControl: no download necessary"); 152 } else { 153 Future<?> future = osmTask.download(settings, new Bounds(minlat, minlon, maxlat, maxlon), 154 null /* let the task manage the progress monitor */); 155 MainApplication.worker.submit(new PostDownloadHandler(osmTask, future)); 156 } 157 } 158 } 159 } catch (RuntimeException ex) { // NOPMD 160 Logging.warn("RemoteControl: Error parsing load_and_zoom remote control request:"); 161 Logging.error(ex); 162 throw new RequestHandlerErrorException(ex); 163 } 164 165 /** 166 * deselect objects if parameter addtags given 167 */ 168 if (args.containsKey("addtags") && !isKeepingCurrentSelection) { 169 GuiHelper.executeByMainWorkerInEDT(() -> { 170 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 171 if (ds == null) // e.g. download failed 172 return; 173 ds.clearSelection(); 174 }); 175 } 176 177 final Collection<OsmPrimitive> forTagAdd = new HashSet<>(); 178 final Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon); 179 if (args.containsKey("select") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) { 180 // select objects after downloading, zoom to selection. 181 GuiHelper.executeByMainWorkerInEDT(() -> { 182 Set<OsmPrimitive> newSel = new HashSet<>(); 183 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 184 if (ds == null) // e.g. download failed 185 return; 186 for (SimplePrimitiveId id : toSelect) { 187 final OsmPrimitive p = ds.getPrimitiveById(id); 188 if (p != null) { 189 newSel.add(p); 190 forTagAdd.add(p); 191 } 192 } 193 if (isKeepingCurrentSelection) { 194 Collection<OsmPrimitive> sel = ds.getSelected(); 195 newSel.addAll(sel); 196 forTagAdd.addAll(sel); 197 } 198 toSelect.clear(); 199 ds.setSelected(newSel); 200 zoom(newSel, bbox); 201 MapFrame map = MainApplication.getMap(); 202 if (MainApplication.isDisplayingMapView() && map.relationListDialog != null) { 203 map.relationListDialog.selectRelations(null); // unselect all relations to fix #7342 204 map.relationListDialog.dataChanged(null); 205 map.relationListDialog.selectRelations(Utils.filteredCollection(newSel, Relation.class)); 206 } 207 }); 208 } else if (args.containsKey("search") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) { 209 try { 210 final SearchCompiler.Match search = SearchCompiler.compile(args.get("search")); 211 MainApplication.worker.submit(() -> { 212 final DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 213 final Collection<OsmPrimitive> filteredPrimitives = SubclassFilteredCollection.filter(ds.allPrimitives(), search); 214 ds.setSelected(filteredPrimitives); 215 forTagAdd.addAll(filteredPrimitives); 216 zoom(filteredPrimitives, bbox); 217 }); 218 } catch (SearchParseError ex) { 219 Logging.error(ex); 220 throw new RequestHandlerErrorException(ex); 221 } 222 } else { 223 // after downloading, zoom to downloaded area. 224 zoom(Collections.<OsmPrimitive>emptySet(), bbox); 225 } 226 227 // This comes before the other changeset tags, so that they can be overridden 228 if (args.containsKey("changeset_tags")) { 229 MainApplication.worker.submit(() -> { 230 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 231 if (ds != null) { 232 for (String[] key : AddTagsDialog.parseUrlTagsToKeyValues(args.get("changeset_tags"))) { 233 ds.addChangeSetTag(key[0], key[1]); 234 } 235 } 236 }); 237 } 238 239 // add changeset tags after download if necessary 240 if (args.containsKey("changeset_comment") || args.containsKey("changeset_source") || args.containsKey("changeset_hashtags")) { 241 MainApplication.worker.submit(() -> { 242 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 243 if (ds != null) { 244 for (String tag : Arrays.asList("changeset_comment", "changeset_source", "changeset_hashtags")) { 245 if (args.containsKey(tag)) { 246 ds.addChangeSetTag(tag.substring("changeset_".length()), args.get(tag)); 247 } 248 } 249 } 250 }); 251 } 252 253 // add tags to objects 254 if (args.containsKey("addtags")) { 255 // needs to run in EDT since forTagAdd is updated in EDT as well 256 GuiHelper.executeByMainWorkerInEDT(() -> { 257 if (!forTagAdd.isEmpty()) { 258 AddTagsDialog.addTags(args, sender, forTagAdd); 259 } else { 260 new Notification(isKeepingCurrentSelection 261 ? tr("You clicked on a JOSM remotecontrol link that would apply tags onto selected objects.\n" 262 + "Since no objects have been selected before this click, no tags were added.\n" 263 + "Select one or more objects and click the link again.") 264 : tr("You clicked on a JOSM remotecontrol link that would apply tags onto objects.\n" 265 + "Unfortunately that link seems to be broken.\n" 266 + "Technical explanation: the URL query parameter ''select='' or ''search='' has an invalid value.\n" 267 + "Ask someone at the origin of the clicked link to fix this.") 268 ).setIcon(JOptionPane.WARNING_MESSAGE).setDuration(Notification.TIME_LONG).show(); 269 } 270 }); 271 } 272 } 273 274 protected void zoom(Collection<OsmPrimitive> primitives, final Bounds bbox) { 275 if (!PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) { 276 return; 277 } 278 // zoom_mode=(download|selection), defaults to selection 279 if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) { 280 AutoScaleAction.autoScale(AutoScaleMode.SELECTION); 281 } else if (MainApplication.isDisplayingMapView()) { 282 // make sure this isn't called unless there *is* a MapView 283 GuiHelper.executeByMainWorkerInEDT(() -> { 284 BoundingXYVisitor bbox1 = new BoundingXYVisitor(); 285 bbox1.visit(bbox); 286 MainApplication.getMap().mapView.zoomTo(bbox1); 287 }); 288 } 289 } 290 291 @Override 292 public PermissionPrefWithDefault getPermissionPref() { 293 return null; 294 } 295 296 @Override 297 protected void validateRequest() throws RequestHandlerBadRequestException { 298 validateDownloadParams(); 299 // Process mandatory arguments 300 minlat = 0; 301 maxlat = 0; 302 minlon = 0; 303 maxlon = 0; 304 try { 305 minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("bottom") : "")); 306 maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("top") : "")); 307 minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("left") : "")); 308 maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("right") : "")); 309 } catch (NumberFormatException e) { 310 throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+')', e); 311 } 312 313 // Current API 0.6 check: "The latitudes must be between -90 and 90" 314 if (!LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat)) { 315 throw new RequestHandlerBadRequestException(tr("The latitudes must be between {0} and {1}", -90d, 90d)); 316 } 317 // Current API 0.6 check: "longitudes between -180 and 180" 318 if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)) { 319 throw new RequestHandlerBadRequestException(tr("The longitudes must be between {0} and {1}", -180d, 180d)); 320 } 321 // Current API 0.6 check: "the minima must be less than the maxima" 322 if (minlat > maxlat || minlon > maxlon) { 323 throw new RequestHandlerBadRequestException(tr("The minima must be less than the maxima")); 324 } 325 326 // Process optional argument 'select' 327 if (args != null && args.containsKey("select")) { 328 toSelect.clear(); 329 for (String item : args.get("select").split(",")) { 330 if (!item.isEmpty()) { 331 if (CURRENT_SELECTION.equalsIgnoreCase(item)) { 332 isKeepingCurrentSelection = true; 333 continue; 334 } 335 try { 336 toSelect.add(SimplePrimitiveId.fromString(item)); 337 } catch (IllegalArgumentException ex) { 338 Logging.log(Logging.LEVEL_WARN, "RemoteControl: invalid selection '" + item + "' ignored", ex); 339 } 340 } 341 } 342 } 343 } 344}