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.size() > 0) {
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", "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,&left=13.740&right=13.741&top=51.05&bottom=51.049",
093                    "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"};
094        } else {
095            return new String[] {
096            "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999",
097            "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&search=highway+OR+railway",
098            };
099        }
100    }
101
102    @Override
103    protected void handleRequest() throws RequestHandlerErrorException {
104        DownloadTask osmTask = new DownloadOsmTask();
105        try {
106            boolean newLayer = isLoadInNewLayer();
107
108            if (command.equals(myCommand)) {
109                if (!PermissionPrefWithDefault.LOAD_DATA.isAllowed()) {
110                    Main.info("RemoteControl: download forbidden by preferences");
111                } else {
112                    Area toDownload = null;
113                    if (!newLayer) {
114                        // find out whether some data has already been downloaded
115                        Area present = null;
116                        DataSet ds = Main.main.getCurrentDataSet();
117                        if (ds != null) {
118                            present = ds.getDataSourceArea();
119                        }
120                        if (present != null && !present.isEmpty()) {
121                            toDownload = new Area(new Rectangle2D.Double(minlon,minlat,maxlon-minlon,maxlat-minlat));
122                            toDownload.subtract(present);
123                            if (!toDownload.isEmpty()) {
124                                // the result might not be a rectangle (L shaped etc)
125                                Rectangle2D downloadBounds = toDownload.getBounds2D();
126                                minlat = downloadBounds.getMinY();
127                                minlon = downloadBounds.getMinX();
128                                maxlat = downloadBounds.getMaxY();
129                                maxlon = downloadBounds.getMaxX();
130                            }
131                        }
132                    }
133                    if (toDownload != null && toDownload.isEmpty()) {
134                        Main.info("RemoteControl: no download necessary");
135                    } else {
136                        Future<?> future = osmTask.download(newLayer, new Bounds(minlat,minlon,maxlat,maxlon), null /* let the task manage the progress monitor */);
137                        Main.worker.submit(new PostDownloadHandler(osmTask, future));
138                    }
139                }
140            }
141        } catch (Exception ex) {
142            Main.warn("RemoteControl: Error parsing load_and_zoom remote control request:");
143            Main.error(ex);
144            throw new RequestHandlerErrorException(ex);
145        }
146
147        /**
148         * deselect objects if parameter addtags given
149         */
150        if (args.containsKey("addtags")) {
151            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
152                @Override
153                public void run() {
154                    DataSet ds = Main.main.getCurrentDataSet();
155                    if(ds == null) // e.g. download failed
156                        return;
157                    ds.clearSelection();
158                }
159            });
160        }
161
162        final Collection<OsmPrimitive> forTagAdd = new HashSet<>();
163        final Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon);
164        if (args.containsKey("select") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
165            // select objects after downloading, zoom to selection.
166            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
167                @Override
168                public void run() {
169                    Set<OsmPrimitive> newSel = new HashSet<>();
170                    DataSet ds = Main.main.getCurrentDataSet();
171                    if (ds == null) // e.g. download failed
172                        return;
173                    for (SimplePrimitiveId id : toSelect) {
174                        final OsmPrimitive p = ds.getPrimitiveById(id);
175                        if (p != null) {
176                            newSel.add(p);
177                            forTagAdd.add(p);
178                        }
179                    }
180                    toSelect.clear();
181                    ds.setSelected(newSel);
182                    zoom(newSel, bbox);
183                    if (Main.isDisplayingMapView() && Main.map.relationListDialog != null) {
184                        Main.map.relationListDialog.selectRelations(null); // unselect all relations to fix #7342
185                        Main.map.relationListDialog.dataChanged(null);
186                        Main.map.relationListDialog.selectRelations(Utils.filteredCollection(newSel, Relation.class));
187                    }
188                }
189            });
190        } else if (args.containsKey("search") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
191            try {
192                final SearchCompiler.Match search = SearchCompiler.compile(args.get("search"), false, false);
193                Main.worker.submit(new Runnable() {
194                    @Override
195                    public void run() {
196                        final DataSet ds = Main.main.getCurrentDataSet();
197                        final Collection<OsmPrimitive> filteredPrimitives = Utils.filter(ds.allPrimitives(), search);
198                        ds.setSelected(filteredPrimitives);
199                        forTagAdd.addAll(filteredPrimitives);
200                        zoom(filteredPrimitives, bbox);
201                    }
202                });
203            } catch (SearchCompiler.ParseError ex) {
204                Main.error(ex);
205                throw new RequestHandlerErrorException(ex);
206            }
207        } else {
208            // after downloading, zoom to downloaded area.
209            zoom(Collections.<OsmPrimitive>emptySet(), bbox);
210        }
211
212        // add changeset tags after download if necessary
213        if (args.containsKey("changeset_comment") || args.containsKey("changeset_source")) {
214            Main.worker.submit(new Runnable() {
215                @Override
216                public void run() {
217                    if (Main.main.getCurrentDataSet() != null) {
218                        if (args.containsKey("changeset_comment")) {
219                            Main.main.getCurrentDataSet().addChangeSetTag("comment", args.get("changeset_comment"));
220                        }
221                        if (args.containsKey("changeset_source")) {
222                            Main.main.getCurrentDataSet().addChangeSetTag("source", args.get("changeset_source"));
223                        }
224                    }
225                }
226            });
227        }
228
229        AddTagsDialog.addTags(args, sender, forTagAdd);
230    }
231
232    protected void zoom(Collection<OsmPrimitive> primitives, final Bounds bbox) {
233        if (!PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) {
234            return;
235        }
236        // zoom_mode=(download|selection), defaults to selection
237        if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) {
238            AutoScaleAction.autoScale("selection");
239        } else if (Main.isDisplayingMapView()) {
240            // make sure this isn't called unless there *is* a MapView
241            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
242                @Override
243                public void run() {
244                    BoundingXYVisitor bbox1 = new BoundingXYVisitor();
245                    bbox1.visit(bbox);
246                    Main.map.mapView.recalculateCenterScale(bbox1);
247                }
248            });
249        }
250    }
251
252    @Override
253    public PermissionPrefWithDefault getPermissionPref() {
254        return null;
255    }
256
257    @Override
258    protected void validateRequest() throws RequestHandlerBadRequestException {
259        // Process mandatory arguments
260        minlat = 0;
261        maxlat = 0;
262        minlon = 0;
263        maxlon = 0;
264        try {
265            minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("bottom")));
266            maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("top")));
267            minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("left")));
268            maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("right")));
269        } catch (NumberFormatException e) {
270            throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+")");
271        }
272
273        // Current API 0.6 check: "The latitudes must be between -90 and 90"
274        if (!LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat)) {
275            throw new RequestHandlerBadRequestException(tr("The latitudes must be between {0} and {1}", -90d, 90d));
276        }
277        // Current API 0.6 check: "longitudes between -180 and 180"
278        if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)) {
279            throw new RequestHandlerBadRequestException(tr("The longitudes must be between {0} and {1}", -180d, 180d));
280        }
281        // Current API 0.6 check: "the minima must be less than the maxima"
282        if (minlat > maxlat || minlon > maxlon) {
283            throw new RequestHandlerBadRequestException(tr("The minima must be less than the maxima"));
284        }
285
286        // Process optional argument 'select'
287        if (args.containsKey("select")) {
288            toSelect.clear();
289            for (String item : args.get("select").split(",")) {
290                try {
291                    toSelect.add(SimplePrimitiveId.fromString(item));
292                } catch (IllegalArgumentException ex) {
293                    Main.warn("RemoteControl: invalid selection '" + item + "' ignored");
294                }
295            }
296        }
297    }
298}