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 Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon);
163        if (args.containsKey("select") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
164            // select objects after downloading, zoom to selection.
165            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
166                @Override
167                public void run() {
168                    Set<OsmPrimitive> newSel = new HashSet<>();
169                    DataSet ds = Main.main.getCurrentDataSet();
170                    if (ds == null) // e.g. download failed
171                        return;
172                    for (SimplePrimitiveId id : toSelect) {
173                        final OsmPrimitive p = ds.getPrimitiveById(id);
174                        if (p != null) {
175                            newSel.add(p);
176                        }
177                    }
178                    toSelect.clear();
179                    ds.setSelected(newSel);
180                    zoom(newSel, bbox);
181                    if (Main.isDisplayingMapView() && Main.map.relationListDialog != null) {
182                        Main.map.relationListDialog.selectRelations(null); // unselect all relations to fix #7342
183                        Main.map.relationListDialog.dataChanged(null);
184                        Main.map.relationListDialog.selectRelations(Utils.filteredCollection(newSel, Relation.class));
185                    }
186                }
187            });
188        } else if (args.containsKey("search") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
189            try {
190                final DataSet ds = Main.main.getCurrentDataSet();
191                final SearchCompiler.Match search = SearchCompiler.compile(args.get("search"), false, false);
192                final Collection<OsmPrimitive> filteredPrimitives = Utils.filter(ds.allPrimitives(), search);
193                ds.setSelected(filteredPrimitives);
194                zoom(filteredPrimitives, bbox);
195            } catch (SearchCompiler.ParseError ex) {
196                Main.error(ex);
197                throw new RequestHandlerErrorException(ex);
198            }
199        } else {
200            // after downloading, zoom to downloaded area.
201            zoom(Collections.<OsmPrimitive>emptySet(), bbox);
202        }
203
204        // add changeset tags after download if necessary
205        if (args.containsKey("changeset_comment") || args.containsKey("changeset_source")) {
206            Main.worker.submit(new Runnable() {
207                @Override
208                public void run() {
209                    if (Main.main.getCurrentDataSet() != null) {
210                        if (args.containsKey("changeset_comment")) {
211                            Main.main.getCurrentDataSet().addChangeSetTag("comment", args.get("changeset_comment"));
212                        }
213                        if (args.containsKey("changeset_source")) {
214                            Main.main.getCurrentDataSet().addChangeSetTag("source", args.get("changeset_source"));
215                        }
216                    }
217                }
218            });
219        }
220
221        AddTagsDialog.addTags(args, sender);
222    }
223
224    protected void zoom(Collection<OsmPrimitive> primitives, final Bounds bbox) {
225        if (!PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) {
226            return;
227        }
228        // zoom_mode=(download|selection), defaults to selection
229        if (!"download".equals(args.get("zoom_mode")) && !primitives.isEmpty()) {
230            AutoScaleAction.autoScale("selection");
231        } else if (Main.isDisplayingMapView()) {
232            // make sure this isn't called unless there *is* a MapView
233            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
234                @Override
235                public void run() {
236                    BoundingXYVisitor bbox1 = new BoundingXYVisitor();
237                    bbox1.visit(bbox);
238                    Main.map.mapView.recalculateCenterScale(bbox1);
239                }
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.get("bottom")));
258            maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("top")));
259            minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("left")));
260            maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("right")));
261        } catch (NumberFormatException e) {
262            throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+")");
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.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("RemoteControl: invalid selection '" + item + "' ignored");
286                }
287            }
288        }
289    }
290}