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