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