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.HashSet;
009import java.util.Set;
010import java.util.concurrent.Future;
011
012import org.openstreetmap.josm.Main;
013import org.openstreetmap.josm.actions.AutoScaleAction;
014import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
015import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
016import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
017import org.openstreetmap.josm.data.Bounds;
018import org.openstreetmap.josm.data.coor.LatLon;
019import org.openstreetmap.josm.data.osm.BBox;
020import org.openstreetmap.josm.data.osm.DataSet;
021import org.openstreetmap.josm.data.osm.Node;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.Way;
025import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
026import org.openstreetmap.josm.gui.util.GuiHelper;
027import org.openstreetmap.josm.io.remotecontrol.AddTagsDialog;
028import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
029import org.openstreetmap.josm.tools.Utils;
030
031/**
032 * Handler for {@code load_and_zoom} and {@code zoom} requests.
033 * @since 3707
034 */
035public class LoadAndZoomHandler extends RequestHandler {
036    
037    /**
038     * The remote control command name used to load data and zoom.
039     */
040    public static final String command = "load_and_zoom";
041
042    /**
043     * The remote control command name used to zoom.
044     */
045    public static final String command2 = "zoom";
046
047    // Mandatory arguments
048    private double minlat;
049    private double maxlat;
050    private double minlon;
051    private double maxlon;
052
053    // Optional argument 'select'
054    private final Set<Long> ways = new HashSet<Long>();
055    private final Set<Long> nodes = new HashSet<Long>();
056    private final Set<Long> relations = new HashSet<Long>();
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") && ways.size()+nodes.size()+relations.size() > 0) {
063            msg += "<br>" + tr("Sel.: Rel.:{0} / Ways:{1} / Nodes:{2}", relations.size(), ways.size(), nodes.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"};
076    }
077
078    @Override
079    public String[] getUsageExamples() {
080        return getUsageExamples(myCommand);
081    }
082    
083    @Override
084    public String[] getUsageExamples(String cmd) {
085        if (command.equals(cmd)) {
086            return new String[] { 
087                    "/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",
088                    "/load_and_zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999&new_layer=true"};
089        } else {
090            return new String[] { 
091            "/zoom?left=8.19&right=8.20&top=48.605&bottom=48.590&select=node413602999"};
092        }
093    }
094
095    @Override
096    protected void handleRequest() throws RequestHandlerErrorException {
097        DownloadTask osmTask = new DownloadOsmTask();
098        try {
099            boolean newLayer = isLoadInNewLayer();
100
101            if (command.equals(myCommand)) {
102                if (!PermissionPrefWithDefault.LOAD_DATA.isAllowed()) {
103                    Main.info("RemoteControl: download forbidden by preferences");
104                } else {
105                    Area toDownload = null;
106                    if (!newLayer) {
107                        // find out whether some data has already been downloaded
108                        Area present = null;
109                        DataSet ds = Main.main.getCurrentDataSet();
110                        if (ds != null) {
111                            present = ds.getDataSourceArea();
112                        }
113                        if (present != null && !present.isEmpty()) {
114                            toDownload = new Area(new Rectangle2D.Double(minlon,minlat,maxlon-minlon,maxlat-minlat));
115                            toDownload.subtract(present);
116                            if (!toDownload.isEmpty()) {
117                                // the result might not be a rectangle (L shaped etc)
118                                Rectangle2D downloadBounds = toDownload.getBounds2D();
119                                minlat = downloadBounds.getMinY();
120                                minlon = downloadBounds.getMinX();
121                                maxlat = downloadBounds.getMaxY();
122                                maxlon = downloadBounds.getMaxX();
123                            }
124                        }
125                    }
126                    if (toDownload != null && toDownload.isEmpty()) {
127                        Main.info("RemoteControl: no download necessary");
128                    } else {
129                        Future<?> future = osmTask.download(newLayer, new Bounds(minlat,minlon,maxlat,maxlon), null /* let the task manage the progress monitor */);
130                        Main.worker.submit(new PostDownloadHandler(osmTask, future));
131                    }
132                }
133            }
134        } catch (Exception ex) {
135            Main.warn("RemoteControl: Error parsing load_and_zoom remote control request:");
136            ex.printStackTrace();
137            throw new RequestHandlerErrorException();
138        }
139
140        /**
141         * deselect objects if parameter addtags given
142         */
143        if (args.containsKey("addtags")) {
144            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
145                @Override
146                public void run() {
147                    DataSet ds = Main.main.getCurrentDataSet();
148                    if(ds == null) // e.g. download failed
149                        return;
150                    ds.clearSelection();
151                }
152            });
153        }
154
155        final Bounds bbox = new Bounds(minlat, minlon, maxlat, maxlon);
156        if (args.containsKey("select") && PermissionPrefWithDefault.CHANGE_SELECTION.isAllowed()) {
157            // select objects after downloading, zoom to selection.
158            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
159                @Override
160                public void run() {
161                    HashSet<OsmPrimitive> newSel = new HashSet<OsmPrimitive>();
162                    DataSet ds = Main.main.getCurrentDataSet();
163                    if(ds == null) // e.g. download failed
164                        return;
165                    for (Way w : ds.getWays()) {
166                        if (ways.contains(w.getId())) {
167                            newSel.add(w);
168                        }
169                    }
170                    ways.clear();
171                    for (Node n : ds.getNodes()) {
172                        if (nodes.contains(n.getId())) {
173                            newSel.add(n);
174                        }
175                    }
176                    nodes.clear();
177                    for (Relation r : ds.getRelations()) {
178                        if (relations.contains(r.getId())) {
179                            newSel.add(r);
180                        }
181                    }
182                    relations.clear();
183                    ds.setSelected(newSel);
184                    if (PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) {
185                        // zoom_mode=(download|selection), defaults to selection
186                        if (!"download".equals(args.get("zoom_mode")) && !newSel.isEmpty()) {
187                            AutoScaleAction.autoScale("selection");
188                        } else {
189                            zoom(bbox);
190                        }
191                    }
192                    if (Main.isDisplayingMapView() && Main.map.relationListDialog != null) {
193                        Main.map.relationListDialog.selectRelations(null); // unselect all relations to fix #7342
194                        Main.map.relationListDialog.dataChanged(null);
195                        Main.map.relationListDialog.selectRelations(Utils.filteredCollection(newSel, Relation.class));
196                    }
197                }
198            });
199        } else if (PermissionPrefWithDefault.CHANGE_VIEWPORT.isAllowed()) {
200            // after downloading, zoom to downloaded area.
201            zoom(bbox);
202        }
203
204        AddTagsDialog.addTags(args, sender);
205    }
206
207    protected void zoom(final Bounds bounds) {
208        // make sure this isn't called unless there *is* a MapView
209        if (Main.isDisplayingMapView()) {
210            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
211                @Override
212                public void run() {
213                    BoundingXYVisitor bbox = new BoundingXYVisitor();
214                    bbox.visit(bounds);
215                    Main.map.mapView.recalculateCenterScale(bbox);
216                }
217            });
218        }
219    }
220
221    @Override
222    public PermissionPrefWithDefault getPermissionPref() {
223        return null;
224    }
225
226    @Override
227    protected void validateRequest() throws RequestHandlerBadRequestException {
228        // Process mandatory arguments
229        minlat = 0;
230        maxlat = 0;
231        minlon = 0;
232        maxlon = 0;
233        try {
234            minlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("bottom")));
235            maxlat = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("top")));
236            minlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("left")));
237            maxlon = LatLon.roundToOsmPrecision(Double.parseDouble(args.get("right")));
238        } catch (NumberFormatException e) {
239            throw new RequestHandlerBadRequestException("NumberFormatException ("+e.getMessage()+")");
240        }
241        
242        // Current API 0.6 check: "The latitudes must be between -90 and 90"
243        if (!LatLon.isValidLat(minlat) || !LatLon.isValidLat(maxlat)) {
244            throw new RequestHandlerBadRequestException(tr("The latitudes must be between {0} and {1}", -90d, 90d));
245        }
246        // Current API 0.6 check: "longitudes between -180 and 180"
247        if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)) {
248            throw new RequestHandlerBadRequestException(tr("The longitudes must be between {0} and {1}", -180d, 180d));
249        }
250        // Current API 0.6 check: "the minima must be less than the maxima"
251        if (minlat > maxlat || minlon > maxlon) {
252            throw new RequestHandlerBadRequestException(tr("The minima must be less than the maxima"));
253        }
254
255        // Process optional argument 'select'
256        if (args.containsKey("select")) {
257            ways.clear();
258            nodes.clear();
259            relations.clear();
260            for (String item : args.get("select").split(",")) {
261                try {
262                    if (item.startsWith("way")) {
263                        ways.add(Long.parseLong(item.substring(3)));
264                    } else if (item.startsWith("node")) {
265                        nodes.add(Long.parseLong(item.substring(4)));
266                    } else if (item.startsWith("relation")) {
267                        relations.add(Long.parseLong(item.substring(8)));
268                    } else if (item.startsWith("rel")) {
269                        relations.add(Long.parseLong(item.substring(3)));
270                    } else {
271                        Main.warn("RemoteControl: invalid selection '"+item+"' ignored");
272                    }
273                } catch (NumberFormatException e) {
274                    Main.warn("RemoteControl: invalid selection '"+item+"' ignored");
275                }
276            }
277        }
278    }
279}