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 // add changeset tags after download if necessary 228 if (args.containsKey("changeset_comment") || args.containsKey("changeset_source") || args.containsKey("changeset_hashtags")) { 229 MainApplication.worker.submit(() -> { 230 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 231 if (ds != null) { 232 for (String tag : Arrays.asList("changeset_comment", "changeset_source", "changeset_hashtags")) { 233 if (args.containsKey(tag)) { 234 ds.addChangeSetTag(tag.substring("changeset_".length()), args.get(tag)); 235 } 236 } 237 } 238 }); 239 } 240 241 // add tags to objects 242 if (args.containsKey("addtags")) { 243 // needs to run in EDT since forTagAdd is updated in EDT as well 244 GuiHelper.executeByMainWorkerInEDT(() -> { 245 if (!forTagAdd.isEmpty()) { 246 AddTagsDialog.addTags(args, sender, forTagAdd); 247 } else { 248 new Notification(isKeepingCurrentSelection 249 ? tr("You clicked on a JOSM remotecontrol link that would apply tags onto selected objects.\n" 250 + "Since no objects have been selected before this click, no tags were added.\n" 251 + "Select one or more objects and click the link again.") 252 : tr("You clicked on a JOSM remotecontrol link that would apply tags onto objects.\n" 253 + "Unfortunately that link seems to be broken.\n" 254 + "Technical explanation: the URL query parameter ''select='' or ''search='' has an invalid value.\n" 255 + "Ask someone at the origin of the clicked link to fix this.") 256 ).setIcon(JOptionPane.WARNING_MESSAGE).setDuration(Notification.TIME_LONG).show(); 257 } 258 }); 259 } 260 } 261 262 protected void zoom(Collection<OsmPrimitive> primitives, final Bounds bbox) { 263 if (!PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) { 264 return; 265 } 266 // zoom_mode=(download|selection), defaults to selection 267 if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) { 268 AutoScaleAction.autoScale(AutoScaleMode.SELECTION); 269 } else if (MainApplication.isDisplayingMapView()) { 270 // make sure this isn't called unless there *is* a MapView 271 GuiHelper.executeByMainWorkerInEDT(() -> { 272 BoundingXYVisitor bbox1 = new BoundingXYVisitor(); 273 bbox1.visit(bbox); 274 MainApplication.getMap().mapView.zoomTo(bbox1); 275 }); 276 } 277 } 278 279 @Override 280 public PermissionPrefWithDefault getPermissionPref() { 281 return null; 282 } 283 284 @Override 285 protected void validateRequest() throws RequestHandlerBadRequestException { 286 validateDownloadParams(); 287 // Process mandatory arguments 288 minlat = 0; 289 maxlat = 0; 290 minlon = 0; 291 maxlon = 0; 292 try { 293 minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("bottom") : "")); 294 maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("top") : "")); 295 minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("left") : "")); 296 maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("right") : "")); 297 } catch (NumberFormatException e) { 298 throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+')', e); 299 } 300 301 // Current API 0.6 check: "The latitudes must be between -90 and 90" 302 if (!LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat)) { 303 throw new RequestHandlerBadRequestException(tr("The latitudes must be between {0} and {1}", -90d, 90d)); 304 } 305 // Current API 0.6 check: "longitudes between -180 and 180" 306 if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)) { 307 throw new RequestHandlerBadRequestException(tr("The longitudes must be between {0} and {1}", -180d, 180d)); 308 } 309 // Current API 0.6 check: "the minima must be less than the maxima" 310 if (minlat > maxlat || minlon > maxlon) { 311 throw new RequestHandlerBadRequestException(tr("The minima must be less than the maxima")); 312 } 313 314 // Process optional argument 'select' 315 if (args != null && args.containsKey("select")) { 316 toSelect.clear(); 317 for (String item : args.get("select").split(",")) { 318 if (!item.isEmpty()) { 319 if (CURRENT_SELECTION.equalsIgnoreCase(item)) { 320 isKeepingCurrentSelection = true; 321 continue; 322 } 323 try { 324 toSelect.add(SimplePrimitiveId.fromString(item)); 325 } catch (IllegalArgumentException ex) { 326 Logging.log(Logging.LEVEL_WARN, "RemoteControl: invalid selection '" + item + "' ignored", ex); 327 } 328 } 329 } 330 } 331 } 332}