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