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