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