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 (Exception 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.get("bottom")));
272            maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("top")));
273            minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("left")));
274            maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(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.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}