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.size() > 0) { 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", "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,&left=13.740&right=13.741&top=51.05&bottom=51.049", 093 "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"}; 094 } else { 095 return new String[] { 096 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999", 097 "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=highway+OR+railway", 098 }; 099 } 100 } 101 102 @Override 103 protected void handleRequest() throws RequestHandlerErrorException { 104 DownloadTask osmTask = new DownloadOsmTask(); 105 try { 106 boolean newLayer = isLoadInNewLayer(); 107 108 if (command.equals(myCommand)) { 109 if (!PermissionPrefWithDefault.LOAD_DATA.isAllowed()) { 110 Main.info("RemoteControl: download forbidden by preferences"); 111 } else { 112 Area toDownload = null; 113 if (!newLayer) { 114 // find out whether some data has already been downloaded 115 Area present = null; 116 DataSet ds = Main.main.getCurrentDataSet(); 117 if (ds != null) { 118 present = ds.getDataSourceArea(); 119 } 120 if (present != null && !present.isEmpty()) { 121 toDownload = new Area(new Rectangle2D.Double(minlon,minlat,maxlon-minlon,maxlat-minlat)); 122 toDownload.subtract(present); 123 if (!toDownload.isEmpty()) { 124 // the result might not be a rectangle (L shaped etc) 125 Rectangle2D downloadBounds = toDownload.getBounds2D(); 126 minlat = downloadBounds.getMinY(); 127 minlon = downloadBounds.getMinX(); 128 maxlat = downloadBounds.getMaxY(); 129 maxlon = downloadBounds.getMaxX(); 130 } 131 } 132 } 133 if (toDownload != null && toDownload.isEmpty()) { 134 Main.info("RemoteControl: no download necessary"); 135 } else { 136 Future<?> future = osmTask.download(newLayer, new Bounds(minlat,minlon,maxlat,maxlon), null /* let the task manage the progress monitor */); 137 Main.worker.submit(new PostDownloadHandler(osmTask, future)); 138 } 139 } 140 } 141 } catch (Exception ex) { 142 Main.warn("RemoteControl: Error parsing load_and_zoom remote control request:"); 143 Main.error(ex); 144 throw new RequestHandlerErrorException(ex); 145 } 146 147 /** 148 * deselect objects if parameter addtags given 149 */ 150 if (args.containsKey("addtags")) { 151 GuiHelper.executeByMainWorkerInEDT(new Runnable() { 152 @Override 153 public void run() { 154 DataSet ds = Main.main.getCurrentDataSet(); 155 if(ds == null) // e.g. download failed 156 return; 157 ds.clearSelection(); 158 } 159 }); 160 } 161 162 final Collection<OsmPrimitive> forTagAdd = new HashSet<>(); 163 final Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon); 164 if (args.containsKey("select") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) { 165 // select objects after downloading, zoom to selection. 166 GuiHelper.executeByMainWorkerInEDT(new Runnable() { 167 @Override 168 public void run() { 169 Set<OsmPrimitive> newSel = new HashSet<>(); 170 DataSet ds = Main.main.getCurrentDataSet(); 171 if (ds == null) // e.g. download failed 172 return; 173 for (SimplePrimitiveId id : toSelect) { 174 final OsmPrimitive p = ds.getPrimitiveById(id); 175 if (p != null) { 176 newSel.add(p); 177 forTagAdd.add(p); 178 } 179 } 180 toSelect.clear(); 181 ds.setSelected(newSel); 182 zoom(newSel, bbox); 183 if (Main.isDisplayingMapView() && Main.map.relationListDialog != null) { 184 Main.map.relationListDialog.selectRelations(null); // unselect all relations to fix #7342 185 Main.map.relationListDialog.dataChanged(null); 186 Main.map.relationListDialog.selectRelations(Utils.filteredCollection(newSel, Relation.class)); 187 } 188 } 189 }); 190 } else if (args.containsKey("search") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) { 191 try { 192 final SearchCompiler.Match search = SearchCompiler.compile(args.get("search"), false, false); 193 Main.worker.submit(new Runnable() { 194 @Override 195 public void run() { 196 final DataSet ds = Main.main.getCurrentDataSet(); 197 final Collection<OsmPrimitive> filteredPrimitives = Utils.filter(ds.allPrimitives(), search); 198 ds.setSelected(filteredPrimitives); 199 forTagAdd.addAll(filteredPrimitives); 200 zoom(filteredPrimitives, bbox); 201 } 202 }); 203 } catch (SearchCompiler.ParseError ex) { 204 Main.error(ex); 205 throw new RequestHandlerErrorException(ex); 206 } 207 } else { 208 // after downloading, zoom to downloaded area. 209 zoom(Collections.<OsmPrimitive>emptySet(), bbox); 210 } 211 212 // add changeset tags after download if necessary 213 if (args.containsKey("changeset_comment") || args.containsKey("changeset_source")) { 214 Main.worker.submit(new Runnable() { 215 @Override 216 public void run() { 217 if (Main.main.getCurrentDataSet() != null) { 218 if (args.containsKey("changeset_comment")) { 219 Main.main.getCurrentDataSet().addChangeSetTag("comment", args.get("changeset_comment")); 220 } 221 if (args.containsKey("changeset_source")) { 222 Main.main.getCurrentDataSet().addChangeSetTag("source", args.get("changeset_source")); 223 } 224 } 225 } 226 }); 227 } 228 229 AddTagsDialog.addTags(args, sender, forTagAdd); 230 } 231 232 protected void zoom(Collection<OsmPrimitive> primitives, final Bounds bbox) { 233 if (!PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) { 234 return; 235 } 236 // zoom_mode=(download|selection), defaults to selection 237 if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) { 238 AutoScaleAction.autoScale("selection"); 239 } else if (Main.isDisplayingMapView()) { 240 // make sure this isn't called unless there *is* a MapView 241 GuiHelper.executeByMainWorkerInEDT(new Runnable() { 242 @Override 243 public void run() { 244 BoundingXYVisitor bbox1 = new BoundingXYVisitor(); 245 bbox1.visit(bbox); 246 Main.map.mapView.zoomTo(bbox1); 247 } 248 }); 249 } 250 } 251 252 @Override 253 public PermissionPrefWithDefault getPermissionPref() { 254 return null; 255 } 256 257 @Override 258 protected void validateRequest() throws RequestHandlerBadRequestException { 259 // Process mandatory arguments 260 minlat = 0; 261 maxlat = 0; 262 minlon = 0; 263 maxlon = 0; 264 try { 265 minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("bottom"))); 266 maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("top"))); 267 minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("left"))); 268 maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("right"))); 269 } catch (NumberFormatException e) { 270 throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+")"); 271 } 272 273 // Current API 0.6 check: "The latitudes must be between -90 and 90" 274 if (!LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat)) { 275 throw new RequestHandlerBadRequestException(tr("The latitudes must be between {0} and {1}", -90d, 90d)); 276 } 277 // Current API 0.6 check: "longitudes between -180 and 180" 278 if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)) { 279 throw new RequestHandlerBadRequestException(tr("The longitudes must be between {0} and {1}", -180d, 180d)); 280 } 281 // Current API 0.6 check: "the minima must be less than the maxima" 282 if (minlat > maxlat || minlon > maxlon) { 283 throw new RequestHandlerBadRequestException(tr("The minima must be less than the maxima")); 284 } 285 286 // Process optional argument 'select' 287 if (args.containsKey("select")) { 288 toSelect.clear(); 289 for (String item : args.get("select").split(",")) { 290 try { 291 toSelect.add(SimplePrimitiveId.fromString(item)); 292 } catch (IllegalArgumentException ex) { 293 Main.warn("RemoteControl: invalid selection '" + item + "' ignored"); 294 } 295 } 296 } 297 } 298}