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}