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 org.openstreetmap.josm.actions.AutoScaleAction; 017import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask; 018import org.openstreetmap.josm.actions.downloadtasks.DownloadTask; 019import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 020import org.openstreetmap.josm.data.Bounds; 021import org.openstreetmap.josm.data.coor.LatLon; 022import org.openstreetmap.josm.data.osm.BBox; 023import org.openstreetmap.josm.data.osm.DataSet; 024import org.openstreetmap.josm.data.osm.OsmPrimitive; 025import org.openstreetmap.josm.data.osm.Relation; 026import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 027import org.openstreetmap.josm.data.osm.search.SearchCompiler; 028import org.openstreetmap.josm.data.osm.search.SearchParseError; 029import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 030import org.openstreetmap.josm.gui.MainApplication; 031import org.openstreetmap.josm.gui.MapFrame; 032import org.openstreetmap.josm.gui.util.GuiHelper; 033import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog; 034import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault; 035import org.openstreetmap.josm.tools.Logging; 036import org.openstreetmap.josm.tools.SubclassFilteredCollection; 037import org.openstreetmap.josm.tools.Utils; 038 039/** 040 * Handler for {@code load_and_zoom} and {@code zoom} requests. 041 * @since 3707 042 */ 043public class LoadAndZoomHandler extends RequestHandler { 044 045 /** 046 * The remote control command name used to load data and zoom. 047 */ 048 public static final String command = "load_and_zoom"; 049 050 /** 051 * The remote control command name used to zoom. 052 */ 053 public static final String command2 = "zoom"; 054 055 // Mandatory arguments 056 private double minlat; 057 private double maxlat; 058 private double minlon; 059 private double maxlon; 060 061 // Optional argument 'select' 062 private final Set<SimplePrimitiveId> toSelect = new HashSet<>(); 063 064 private boolean isKeepingCurrentSelection; 065 066 @Override 067 public String getPermissionMessage() { 068 String msg = tr("Remote Control has been asked to load data from the API.") + 069 "<br>" + tr("Bounding box: ") + new BBox(minlon, minlat, maxlon, maxlat).toStringCSV(", "); 070 if (args.containsKey("select") && !toSelect.isEmpty()) { 071 msg += "<br>" + tr("Selection: {0}", toSelect.size()); 072 } 073 return msg; 074 } 075 076 @Override 077 public String[] getMandatoryParams() { 078 return new String[] {"bottom", "top", "left", "right"}; 079 } 080 081 @Override 082 public String[] getOptionalParams() { 083 return new String[] {"new_layer", "layer_name", "addtags", "select", "zoom_mode", 084 "changeset_comment", "changeset_source", "changeset_hashtags", "search"}; 085 } 086 087 @Override 088 public String getUsage() { 089 return "download a bounding box from the API, zoom to the downloaded area and optionally select one or more objects"; 090 } 091 092 @Override 093 public String[] getUsageExamples() { 094 return getUsageExamples(myCommand); 095 } 096 097 @Override 098 public String[] getUsageExamples(String cmd) { 099 if (command.equals(cmd)) { 100 return new String[] { 101 "/load_and_zoom?addtags=wikipedia:de=Wei%C3%9Fe_Gasse|maxspeed=5&select=way23071688,way23076176,way23076177," + 102 "&left=13.740&right=13.741&top=51.05&bottom=51.049", 103 "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"}; 104 } else { 105 return new String[] { 106 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999", 107 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=highway+OR+railway", 108 }; 109 } 110 } 111 112 @Override 113 protected void handleRequest() throws RequestHandlerErrorException { 114 DownloadTask osmTask = new DownloadOsmTask() { 115 { 116 newLayerName = args.get("layer_name"); 117 } 118 }; 119 try { 120 boolean newLayer = isLoadInNewLayer(); 121 122 if (command.equals(myCommand)) { 123 if (!PermissionPrefWithDefault.LOAD_DATA.isAllowed()) { 124 Logging.info("RemoteControl: download forbidden by preferences"); 125 } else { 126 Area toDownload = null; 127 if (!newLayer) { 128 // find out whether some data has already been downloaded 129 Area present = null; 130 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 131 if (ds != null) { 132 present = ds.getDataSourceArea(); 133 } 134 if (present != null && !present.isEmpty()) { 135 toDownload = new Area(new Rectangle2D.Double(minlon, minlat, maxlon-minlon, maxlat-minlat)); 136 toDownload.subtract(present); 137 if (!toDownload.isEmpty()) { 138 // the result might not be a rectangle (L shaped etc) 139 Rectangle2D downloadBounds = toDownload.getBounds2D(); 140 minlat = downloadBounds.getMinY(); 141 minlon = downloadBounds.getMinX(); 142 maxlat = downloadBounds.getMaxY(); 143 maxlon = downloadBounds.getMaxX(); 144 } 145 } 146 } 147 if (toDownload != null && toDownload.isEmpty()) { 148 Logging.info("RemoteControl: no download necessary"); 149 } else { 150 Future<?> future = osmTask.download(newLayer, new Bounds(minlat, minlon, maxlat, maxlon), 151 null /* let the task manage the progress monitor */); 152 MainApplication.worker.submit(new PostDownloadHandler(osmTask, future)); 153 } 154 } 155 } 156 } catch (RuntimeException ex) { // NOPMD 157 Logging.warn("RemoteControl: Error parsing load_and_zoom remote control request:"); 158 Logging.error(ex); 159 throw new RequestHandlerErrorException(ex); 160 } 161 162 /** 163 * deselect objects if parameter addtags given 164 */ 165 if (args.containsKey("addtags") && !isKeepingCurrentSelection) { 166 GuiHelper.executeByMainWorkerInEDT(() -> { 167 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 168 if (ds == null) // e.g. download failed 169 return; 170 ds.clearSelection(); 171 }); 172 } 173 174 final Collection<OsmPrimitive> forTagAdd = new HashSet<>(); 175 final Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon); 176 if (args.containsKey("select") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) { 177 // select objects after downloading, zoom to selection. 178 GuiHelper.executeByMainWorkerInEDT(() -> { 179 Set<OsmPrimitive> newSel = new HashSet<>(); 180 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 181 if (ds == null) // e.g. download failed 182 return; 183 for (SimplePrimitiveId id : toSelect) { 184 final OsmPrimitive p = ds.getPrimitiveById(id); 185 if (p != null) { 186 newSel.add(p); 187 forTagAdd.add(p); 188 } 189 } 190 if (isKeepingCurrentSelection) { 191 Collection<OsmPrimitive> sel = ds.getSelected(); 192 newSel.addAll(sel); 193 forTagAdd.addAll(sel); 194 } 195 toSelect.clear(); 196 isKeepingCurrentSelection = false; 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 AddTagsDialog.addTags(args, sender, forTagAdd); 240 } 241 242 protected void zoom(Collection<OsmPrimitive> primitives, final Bounds bbox) { 243 if (!PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) { 244 return; 245 } 246 // zoom_mode=(download|selection), defaults to selection 247 if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) { 248 AutoScaleAction.autoScale("selection"); 249 } else if (MainApplication.isDisplayingMapView()) { 250 // make sure this isn't called unless there *is* a MapView 251 GuiHelper.executeByMainWorkerInEDT(() -> { 252 BoundingXYVisitor bbox1 = new BoundingXYVisitor(); 253 bbox1.visit(bbox); 254 MainApplication.getMap().mapView.zoomTo(bbox1); 255 }); 256 } 257 } 258 259 @Override 260 public PermissionPrefWithDefault getPermissionPref() { 261 return null; 262 } 263 264 @Override 265 protected void validateRequest() throws RequestHandlerBadRequestException { 266 // Process mandatory arguments 267 minlat = 0; 268 maxlat = 0; 269 minlon = 0; 270 maxlon = 0; 271 try { 272 minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("bottom") : "")); 273 maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("top") : "")); 274 minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("left") : "")); 275 maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args != null ? args.get("right") : "")); 276 } catch (NumberFormatException e) { 277 throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+')', e); 278 } 279 280 // Current API 0.6 check: "The latitudes must be between -90 and 90" 281 if (!LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat)) { 282 throw new RequestHandlerBadRequestException(tr("The latitudes must be between {0} and {1}", -90d, 90d)); 283 } 284 // Current API 0.6 check: "longitudes between -180 and 180" 285 if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)) { 286 throw new RequestHandlerBadRequestException(tr("The longitudes must be between {0} and {1}", -180d, 180d)); 287 } 288 // Current API 0.6 check: "the minima must be less than the maxima" 289 if (minlat > maxlat || minlon > maxlon) { 290 throw new RequestHandlerBadRequestException(tr("The minima must be less than the maxima")); 291 } 292 293 // Process optional argument 'select' 294 if (args != null && args.containsKey("select")) { 295 toSelect.clear(); 296 for (String item : args.get("select").split(",")) { 297 if (!item.isEmpty()) { 298 if ("currentselection".equals(item.toLowerCase(Locale.ENGLISH))) { 299 isKeepingCurrentSelection = true; 300 continue; 301 } 302 try { 303 toSelect.add(SimplePrimitiveId.fromString(item)); 304 } catch (IllegalArgumentException ex) { 305 Logging.log(Logging.LEVEL_WARN, "RemoteControl: invalid selection '" + item + "' ignored", ex); 306 } 307 } 308 } 309 } 310 } 311}