001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GraphicsEnvironment;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionEvent;
009import java.awt.geom.Area;
010import java.awt.geom.Path2D;
011import java.awt.geom.PathIterator;
012import java.awt.geom.Rectangle2D;
013import java.util.ArrayList;
014import java.util.Collection;
015import java.util.List;
016import java.util.concurrent.Future;
017
018import javax.swing.JLabel;
019import javax.swing.JOptionPane;
020import javax.swing.JPanel;
021
022import org.openstreetmap.josm.actions.downloadtasks.DownloadTaskList;
023import org.openstreetmap.josm.data.coor.LatLon;
024import org.openstreetmap.josm.data.osm.DataSet;
025import org.openstreetmap.josm.gui.MainApplication;
026import org.openstreetmap.josm.gui.PleaseWaitRunnable;
027import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongPanel;
028import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
029import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
030import org.openstreetmap.josm.spi.preferences.Config;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.Logging;
033import org.openstreetmap.josm.tools.Shortcut;
034import org.openstreetmap.josm.tools.Utils;
035
036/**
037 * Abstract superclass of DownloadAlongTrackAction and DownloadAlongWayAction
038 * @since 6054
039 */
040public abstract class DownloadAlongAction extends JosmAction {
041
042    /**
043     * Sub classes must override this method.
044     * @return the task to start or null if nothing to do
045     */
046    protected abstract PleaseWaitRunnable createTask();
047
048    /**
049     * Constructs a new {@code DownloadAlongAction}
050     * @param name the action's text as displayed in the menu
051     * @param iconName the filename of the icon to use
052     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
053     *           that html is not supported for menu actions on some platforms.
054     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
055     *            do want a shortcut, remember you can always register it with group=none, so you
056     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
057     *            the user CANNOT configure a shortcut for your action.
058     * @param registerInToolbar register this action for the toolbar preferences?
059     */
060    public DownloadAlongAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) {
061        super(name, iconName, tooltip, shortcut, registerInToolbar);
062    }
063
064    protected static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double maxArea) {
065        Area tmp = new Area(r);
066        // intersect with sought-after area
067        tmp.intersect(a);
068        if (tmp.isEmpty()) {
069            return;
070        }
071        Rectangle2D bounds = tmp.getBounds2D();
072        if (bounds.getWidth() * bounds.getHeight() > maxArea) {
073            // the rectangle gets too large; split it and make recursive call.
074            Rectangle2D r1;
075            Rectangle2D r2;
076            if (bounds.getWidth() > bounds.getHeight()) {
077                // rectangles that are wider than high are split into a left and right half,
078                r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth() / 2, bounds.getHeight());
079                r2 = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 2, bounds.getY(),
080                        bounds.getWidth() / 2, bounds.getHeight());
081            } else {
082                // others into a top and bottom half.
083                r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() / 2);
084                r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY() + bounds.getHeight() / 2, bounds.getWidth(),
085                        bounds.getHeight() / 2);
086            }
087            addToDownload(tmp, r1, results, maxArea);
088            addToDownload(tmp, r2, results, maxArea);
089        } else {
090            DataSet ds = MainApplication.getLayerManager().getEditDataSet();
091            if (ds != null) {
092                double p = LatLon.MAX_SERVER_PRECISION;
093                LatLon min = new LatLon(bounds.getY()+p, bounds.getX()+p);
094                LatLon max = new LatLon(bounds.getY()+bounds.getHeight()-p, bounds.getX()+bounds.getWidth()-p);
095                if (ds.getDataSourceBounds().stream().anyMatch(current -> (current.contains(min) && current.contains(max)))) {
096                    return; // skip this one, already downloaded
097                }
098            }
099            results.add(bounds);
100        }
101    }
102
103    /**
104     * Area "a" contains the hull that we would like to download data for. however we
105     * can only download rectangles, so the following is an attempt at finding a number of
106     * rectangles to download.
107     *
108     * The idea is simply: Start out with the full bounding box. If it is too large, then
109     * split it in half and repeat recursively for each half until you arrive at something
110     * small enough to download. The algorithm is improved by always using the intersection
111     * between the rectangle and the actual desired area. For example, if you have a track
112     * that goes like this: +----+ | /| | / | | / | |/ | +----+ then we would first look at
113     * downloading the whole rectangle (assume it's too big), after that we split it in half
114     * (upper and lower half), but we do not request the full upper and lower rectangle, only
115     * the part of the upper/lower rectangle that actually has something in it.
116     *
117     * This functions calculates the rectangles, asks the user to continue and downloads
118     * the areas if applicable.
119     *
120     * @param a download area hull
121     * @param maxArea maximum area size for a single download
122     * @param osmDownload Set to true if OSM data should be downloaded
123     * @param gpxDownload Set to true if GPX data should be downloaded
124     * @param title the title string for the confirmation dialog
125     * @param newLayer Set to true if all areas should be put into a single new layer
126     */
127    protected static void confirmAndDownloadAreas(Area a, double maxArea, boolean osmDownload, boolean gpxDownload, String title,
128            boolean newLayer) {
129        List<Rectangle2D> toDownload = new ArrayList<>();
130        addToDownload(a, a.getBounds(), toDownload, maxArea);
131        if (toDownload.isEmpty()) {
132            return;
133        }
134        JPanel msg = new JPanel(new GridBagLayout());
135        msg.add(new JLabel(
136                tr("<html>This action will require {0} individual<br>" + "download requests. Do you wish<br>to continue?</html>",
137                        toDownload.size())), GBC.eol());
138        if (!GraphicsEnvironment.isHeadless() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(
139                MainApplication.getMainFrame(), msg, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) {
140            return;
141        }
142        final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Download data"));
143        final Future<?> future = new DownloadTaskList(Config.getPref().getBoolean("download.along.zoom-after-download"))
144                .download(newLayer, toDownload, osmDownload, gpxDownload, monitor);
145        waitFuture(future, monitor);
146    }
147
148    /**
149     * Calculate list of points between two given points so that the distance between two consecutive points is below a limit.
150     * @param p1 first point or null
151     * @param p2 second point (must not be null)
152     * @param bufferDist the maximum distance
153     * @return a list of points with at least one point (p2) and maybe more.
154     */
155    protected static Collection<LatLon> calcBetweenPoints(LatLon p1, LatLon p2, double bufferDist) {
156        ArrayList<LatLon> intermediateNodes = new ArrayList<>();
157        intermediateNodes.add(p2);
158        if (p1 != null && p2.greatCircleDistance(p1) > bufferDist) {
159            Double d = p2.greatCircleDistance(p1) / bufferDist;
160            int nbNodes = d.intValue();
161            if (Logging.isDebugEnabled()) {
162                Logging.debug(tr("{0} intermediate nodes to download.", nbNodes));
163                Logging.debug(tr("between {0} {1} and {2} {3}", p2.lat(), p2.lon(), p1.lat(), p1.lon()));
164            }
165            double latStep = (p2.lat() - p1.lat()) / (nbNodes + 1);
166            double lonStep = (p2.lon() - p1.lon()) / (nbNodes + 1);
167            for (int i = 1; i <= nbNodes; i++) {
168                LatLon intermediate = new LatLon(p1.lat() + i * latStep, p1.lon() + i * lonStep);
169                intermediateNodes.add(intermediate);
170                if (Logging.isTraceEnabled()) {
171                    Logging.trace(tr("  adding {0} {1}", intermediate.lat(), intermediate.lon()));
172                }
173            }
174        }
175        return intermediateNodes;
176    }
177
178    /**
179     * Create task that downloads areas along the given path using the values specified in the panel.
180     * @param alongPath the path along which the areas are to be downloaded
181     * @param panel the panel that was displayed to the user and now contains his selections
182     * @param confirmTitle the title to display in the confirmation panel
183     * @param newLayer Set to true if all areas should be put into a single new layer
184     * @return the task or null if canceled by user
185     */
186    protected PleaseWaitRunnable createCalcTask(Path2D alongPath, DownloadAlongPanel panel, String confirmTitle, boolean newLayer) {
187        /*
188         * Find the average latitude for the data we're contemplating, so we can know how many
189         * metres per degree of longitude we have.
190         */
191        double latsum = 0;
192        int latcnt = 0;
193        final PathIterator pit = alongPath.getPathIterator(null);
194        final double[] res = new double[6];
195        while (!pit.isDone()) {
196            int type = pit.currentSegment(res);
197            if (type == PathIterator.SEG_LINETO || type == PathIterator.SEG_MOVETO) {
198                latsum += res[1];
199                latcnt++;
200            }
201            pit.next();
202        }
203        if (latcnt == 0) {
204            return null;
205        }
206        final double avglat = latsum / latcnt;
207        final double scale = Math.cos(Utils.toRadians(avglat));
208
209        /*
210         * Compute buffer zone extents and maximum bounding box size. Note that the maximum we
211         * ever offer is a bbox area of 0.002, while the API theoretically supports 0.25, but as
212         * soon as you touch any built-up area, that kind of bounding box will download forever
213         * and then stop because it has more than 50k nodes.
214         */
215        final double bufferDist = panel.getDistance();
216        final double maxArea = panel.getArea() / 10000.0 / scale;
217        final double bufferY = bufferDist / 100000.0;
218        final double bufferX = bufferY / scale;
219        final int totalTicks = latcnt;
220        // guess if a progress bar might be useful.
221        final boolean displayProgress = totalTicks > 20_000 && bufferY < 0.01;
222
223        class CalculateDownloadArea extends PleaseWaitRunnable {
224
225            private final Path2D downloadPath = new Path2D.Double();
226            private final boolean newLayer;
227            private boolean cancel;
228            private int ticks;
229            private final Rectangle2D r = new Rectangle2D.Double();
230
231            CalculateDownloadArea(boolean newLayer) {
232                super(tr("Calculating Download Area"), displayProgress ? null : NullProgressMonitor.INSTANCE, false);
233                this.newLayer = newLayer;
234            }
235
236            @Override
237            protected void cancel() {
238                cancel = true;
239            }
240
241            @Override
242            protected void finish() {
243                // Do nothing
244            }
245
246            @Override
247            protected void afterFinish() {
248                if (cancel) {
249                    return;
250                }
251                confirmAndDownloadAreas(new Area(downloadPath), maxArea, panel.isDownloadOsmData(), panel.isDownloadGpxData(),
252                        confirmTitle, newLayer);
253            }
254
255            /**
256             * increase tick count by one, report progress every 100 ticks
257             */
258            private void tick() {
259                ticks++;
260                if (ticks % 100 == 0) {
261                    progressMonitor.worked(100);
262                }
263            }
264
265            /**
266             * calculate area enclosing a single point
267             */
268            private void calcAreaForWayPoint(LatLon c) {
269                r.setRect(c.lon() - bufferX, c.lat() - bufferY, 2 * bufferX, 2 * bufferY);
270                downloadPath.append(r, false);
271            }
272
273            @Override
274            protected void realRun() {
275                progressMonitor.setTicksCount(totalTicks);
276                PathIterator pit = alongPath.getPathIterator(null);
277                double[] res = new double[6];
278                LatLon previous = null;
279                while (!pit.isDone()) {
280                    int type = pit.currentSegment(res);
281                    LatLon c = new LatLon(res[1], res[0]);
282                    if (type == PathIterator.SEG_LINETO) {
283                        tick();
284                        for (LatLon d : calcBetweenPoints(previous, c, bufferDist)) {
285                            calcAreaForWayPoint(d);
286                        }
287                        previous = c;
288                    } else if (type == PathIterator.SEG_MOVETO) {
289                        previous = c;
290                        tick();
291                        calcAreaForWayPoint(c);
292                    }
293                    pit.next();
294                }
295            }
296        }
297
298        return new CalculateDownloadArea(newLayer);
299    }
300
301    @Override
302    public void actionPerformed(ActionEvent e) {
303        PleaseWaitRunnable task = createTask();
304        if (task != null) {
305            MainApplication.worker.submit(task);
306        }
307    }
308}