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