[7937] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.actions;
|
---|
| 3 |
|
---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
[16066] | 5 | import static org.openstreetmap.josm.tools.I18n.trn;
|
---|
[7937] | 6 |
|
---|
[9804] | 7 | import java.awt.GraphicsEnvironment;
|
---|
[7937] | 8 | import java.awt.GridBagLayout;
|
---|
[14962] | 9 | import java.awt.event.ActionEvent;
|
---|
[7937] | 10 | import java.awt.geom.Area;
|
---|
[14962] | 11 | import java.awt.geom.Path2D;
|
---|
| 12 | import java.awt.geom.PathIterator;
|
---|
[7937] | 13 | import java.awt.geom.Rectangle2D;
|
---|
[16068] | 14 | import java.text.MessageFormat;
|
---|
[7937] | 15 | import java.util.ArrayList;
|
---|
| 16 | import java.util.Collection;
|
---|
| 17 | import java.util.List;
|
---|
| 18 | import java.util.concurrent.Future;
|
---|
| 19 |
|
---|
| 20 | import javax.swing.JLabel;
|
---|
| 21 | import javax.swing.JOptionPane;
|
---|
| 22 | import javax.swing.JPanel;
|
---|
| 23 |
|
---|
| 24 | import org.openstreetmap.josm.actions.downloadtasks.DownloadTaskList;
|
---|
[14962] | 25 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
[15340] | 26 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
[14153] | 27 | import org.openstreetmap.josm.gui.MainApplication;
|
---|
[14962] | 28 | import org.openstreetmap.josm.gui.PleaseWaitRunnable;
|
---|
| 29 | import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongPanel;
|
---|
| 30 | import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
|
---|
[12675] | 31 | import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
|
---|
[15205] | 32 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
[7937] | 33 | import org.openstreetmap.josm.tools.GBC;
|
---|
[14962] | 34 | import org.openstreetmap.josm.tools.Logging;
|
---|
[7937] | 35 | import org.openstreetmap.josm.tools.Shortcut;
|
---|
[14962] | 36 | import org.openstreetmap.josm.tools.Utils;
|
---|
[7937] | 37 |
|
---|
| 38 | /**
|
---|
| 39 | * Abstract superclass of DownloadAlongTrackAction and DownloadAlongWayAction
|
---|
| 40 | * @since 6054
|
---|
| 41 | */
|
---|
| 42 | public abstract class DownloadAlongAction extends JosmAction {
|
---|
| 43 |
|
---|
| 44 | /**
|
---|
[14962] | 45 | * Sub classes must override this method.
|
---|
| 46 | * @return the task to start or null if nothing to do
|
---|
| 47 | */
|
---|
| 48 | protected abstract PleaseWaitRunnable createTask();
|
---|
| 49 |
|
---|
| 50 | /**
|
---|
[7937] | 51 | * Constructs a new {@code DownloadAlongAction}
|
---|
| 52 | * @param name the action's text as displayed in the menu
|
---|
| 53 | * @param iconName the filename of the icon to use
|
---|
| 54 | * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
|
---|
| 55 | * that html is not supported for menu actions on some platforms.
|
---|
| 56 | * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
|
---|
| 57 | * do want a shortcut, remember you can always register it with group=none, so you
|
---|
| 58 | * won't be assigned a shortcut unless the user configures one. If you pass null here,
|
---|
| 59 | * the user CANNOT configure a shortcut for your action.
|
---|
| 60 | * @param registerInToolbar register this action for the toolbar preferences?
|
---|
| 61 | */
|
---|
[16553] | 62 | protected DownloadAlongAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) {
|
---|
[7937] | 63 | super(name, iconName, tooltip, shortcut, registerInToolbar);
|
---|
| 64 | }
|
---|
[8509] | 65 |
|
---|
[7937] | 66 | protected static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double maxArea) {
|
---|
| 67 | Area tmp = new Area(r);
|
---|
| 68 | // intersect with sought-after area
|
---|
| 69 | tmp.intersect(a);
|
---|
| 70 | if (tmp.isEmpty()) {
|
---|
| 71 | return;
|
---|
| 72 | }
|
---|
| 73 | Rectangle2D bounds = tmp.getBounds2D();
|
---|
| 74 | if (bounds.getWidth() * bounds.getHeight() > maxArea) {
|
---|
| 75 | // the rectangle gets too large; split it and make recursive call.
|
---|
| 76 | Rectangle2D r1;
|
---|
| 77 | Rectangle2D r2;
|
---|
| 78 | if (bounds.getWidth() > bounds.getHeight()) {
|
---|
| 79 | // rectangles that are wider than high are split into a left and right half,
|
---|
| 80 | r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth() / 2, bounds.getHeight());
|
---|
| 81 | r2 = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 2, bounds.getY(),
|
---|
| 82 | bounds.getWidth() / 2, bounds.getHeight());
|
---|
| 83 | } else {
|
---|
| 84 | // others into a top and bottom half.
|
---|
| 85 | r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() / 2);
|
---|
| 86 | r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY() + bounds.getHeight() / 2, bounds.getWidth(),
|
---|
| 87 | bounds.getHeight() / 2);
|
---|
| 88 | }
|
---|
[14868] | 89 | addToDownload(tmp, r1, results, maxArea);
|
---|
| 90 | addToDownload(tmp, r2, results, maxArea);
|
---|
[7937] | 91 | } else {
|
---|
[15340] | 92 | DataSet ds = MainApplication.getLayerManager().getEditDataSet();
|
---|
| 93 | if (ds != null) {
|
---|
[15350] | 94 | double p = LatLon.MAX_SERVER_PRECISION;
|
---|
| 95 | LatLon min = new LatLon(bounds.getY()+p, bounds.getX()+p);
|
---|
| 96 | LatLon max = new LatLon(bounds.getY()+bounds.getHeight()-p, bounds.getX()+bounds.getWidth()-p);
|
---|
| 97 | if (ds.getDataSourceBounds().stream().anyMatch(current -> (current.contains(min) && current.contains(max)))) {
|
---|
| 98 | return; // skip this one, already downloaded
|
---|
[15340] | 99 | }
|
---|
| 100 | }
|
---|
[7937] | 101 | results.add(bounds);
|
---|
| 102 | }
|
---|
| 103 | }
|
---|
[8509] | 104 |
|
---|
[7937] | 105 | /**
|
---|
| 106 | * Area "a" contains the hull that we would like to download data for. however we
|
---|
| 107 | * can only download rectangles, so the following is an attempt at finding a number of
|
---|
| 108 | * rectangles to download.
|
---|
| 109 | *
|
---|
| 110 | * The idea is simply: Start out with the full bounding box. If it is too large, then
|
---|
| 111 | * split it in half and repeat recursively for each half until you arrive at something
|
---|
| 112 | * small enough to download. The algorithm is improved by always using the intersection
|
---|
| 113 | * between the rectangle and the actual desired area. For example, if you have a track
|
---|
| 114 | * that goes like this: +----+ | /| | / | | / | |/ | +----+ then we would first look at
|
---|
| 115 | * downloading the whole rectangle (assume it's too big), after that we split it in half
|
---|
[15340] | 116 | * (upper and lower half), but we do not request the full upper and lower rectangle, only
|
---|
[7937] | 117 | * the part of the upper/lower rectangle that actually has something in it.
|
---|
| 118 | *
|
---|
| 119 | * This functions calculates the rectangles, asks the user to continue and downloads
|
---|
| 120 | * the areas if applicable.
|
---|
[8926] | 121 | *
|
---|
| 122 | * @param a download area hull
|
---|
| 123 | * @param maxArea maximum area size for a single download
|
---|
| 124 | * @param osmDownload Set to true if OSM data should be downloaded
|
---|
| 125 | * @param gpxDownload Set to true if GPX data should be downloaded
|
---|
| 126 | * @param title the title string for the confirmation dialog
|
---|
[15363] | 127 | * @param newLayer Set to true if all areas should be put into a single new layer
|
---|
[7937] | 128 | */
|
---|
[15363] | 129 | protected static void confirmAndDownloadAreas(Area a, double maxArea, boolean osmDownload, boolean gpxDownload, String title,
|
---|
| 130 | boolean newLayer) {
|
---|
[7937] | 131 | List<Rectangle2D> toDownload = new ArrayList<>();
|
---|
| 132 | addToDownload(a, a.getBounds(), toDownload, maxArea);
|
---|
| 133 | if (toDownload.isEmpty()) {
|
---|
| 134 | return;
|
---|
| 135 | }
|
---|
[16066] | 136 | if (toDownload.size() > 1) {
|
---|
| 137 | JPanel msg = new JPanel(new GridBagLayout());
|
---|
| 138 | msg.add(new JLabel(trn(
|
---|
| 139 | "<html>This action will require {0} individual<br>download request. Do you wish<br>to continue?</html>",
|
---|
| 140 | "<html>This action will require {0} individual<br>download requests. Do you wish<br>to continue?</html>",
|
---|
| 141 | toDownload.size(), toDownload.size())), GBC.eol());
|
---|
| 142 | if (!GraphicsEnvironment.isHeadless() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(
|
---|
| 143 | MainApplication.getMainFrame(), msg, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) {
|
---|
| 144 | return;
|
---|
| 145 | }
|
---|
[7937] | 146 | }
|
---|
| 147 | final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Download data"));
|
---|
[15205] | 148 | final Future<?> future = new DownloadTaskList(Config.getPref().getBoolean("download.along.zoom-after-download"))
|
---|
[15363] | 149 | .download(newLayer, toDownload, osmDownload, gpxDownload, monitor);
|
---|
[10074] | 150 | waitFuture(future, monitor);
|
---|
[7937] | 151 | }
|
---|
[14962] | 152 |
|
---|
| 153 | /**
|
---|
| 154 | * Calculate list of points between two given points so that the distance between two consecutive points is below a limit.
|
---|
| 155 | * @param p1 first point or null
|
---|
| 156 | * @param p2 second point (must not be null)
|
---|
| 157 | * @param bufferDist the maximum distance
|
---|
| 158 | * @return a list of points with at least one point (p2) and maybe more.
|
---|
| 159 | */
|
---|
| 160 | protected static Collection<LatLon> calcBetweenPoints(LatLon p1, LatLon p2, double bufferDist) {
|
---|
| 161 | ArrayList<LatLon> intermediateNodes = new ArrayList<>();
|
---|
| 162 | intermediateNodes.add(p2);
|
---|
| 163 | if (p1 != null && p2.greatCircleDistance(p1) > bufferDist) {
|
---|
[16068] | 164 | double d = p2.greatCircleDistance(p1) / bufferDist;
|
---|
| 165 | int nbNodes = (int) d;
|
---|
[14962] | 166 | if (Logging.isDebugEnabled()) {
|
---|
[16068] | 167 | Logging.debug(MessageFormat.format("{0} intermediate nodes to download between {1} and {2}", nbNodes, p2, p1));
|
---|
[14962] | 168 | }
|
---|
| 169 | double latStep = (p2.lat() - p1.lat()) / (nbNodes + 1);
|
---|
| 170 | double lonStep = (p2.lon() - p1.lon()) / (nbNodes + 1);
|
---|
| 171 | for (int i = 1; i <= nbNodes; i++) {
|
---|
| 172 | LatLon intermediate = new LatLon(p1.lat() + i * latStep, p1.lon() + i * lonStep);
|
---|
| 173 | intermediateNodes.add(intermediate);
|
---|
| 174 | if (Logging.isTraceEnabled()) {
|
---|
| 175 | Logging.trace(tr(" adding {0} {1}", intermediate.lat(), intermediate.lon()));
|
---|
| 176 | }
|
---|
| 177 | }
|
---|
| 178 | }
|
---|
| 179 | return intermediateNodes;
|
---|
| 180 | }
|
---|
| 181 |
|
---|
| 182 | /**
|
---|
| 183 | * Create task that downloads areas along the given path using the values specified in the panel.
|
---|
| 184 | * @param alongPath the path along which the areas are to be downloaded
|
---|
| 185 | * @param panel the panel that was displayed to the user and now contains his selections
|
---|
| 186 | * @param confirmTitle the title to display in the confirmation panel
|
---|
[15363] | 187 | * @param newLayer Set to true if all areas should be put into a single new layer
|
---|
[14962] | 188 | * @return the task or null if canceled by user
|
---|
| 189 | */
|
---|
[15363] | 190 | protected PleaseWaitRunnable createCalcTask(Path2D alongPath, DownloadAlongPanel panel, String confirmTitle, boolean newLayer) {
|
---|
[14962] | 191 | /*
|
---|
| 192 | * Find the average latitude for the data we're contemplating, so we can know how many
|
---|
| 193 | * metres per degree of longitude we have.
|
---|
| 194 | */
|
---|
| 195 | double latsum = 0;
|
---|
| 196 | int latcnt = 0;
|
---|
| 197 | final PathIterator pit = alongPath.getPathIterator(null);
|
---|
| 198 | final double[] res = new double[6];
|
---|
| 199 | while (!pit.isDone()) {
|
---|
| 200 | int type = pit.currentSegment(res);
|
---|
| 201 | if (type == PathIterator.SEG_LINETO || type == PathIterator.SEG_MOVETO) {
|
---|
| 202 | latsum += res[1];
|
---|
| 203 | latcnt++;
|
---|
| 204 | }
|
---|
| 205 | pit.next();
|
---|
| 206 | }
|
---|
| 207 | if (latcnt == 0) {
|
---|
| 208 | return null;
|
---|
| 209 | }
|
---|
| 210 | final double avglat = latsum / latcnt;
|
---|
| 211 | final double scale = Math.cos(Utils.toRadians(avglat));
|
---|
| 212 |
|
---|
| 213 | /*
|
---|
| 214 | * Compute buffer zone extents and maximum bounding box size. Note that the maximum we
|
---|
| 215 | * ever offer is a bbox area of 0.002, while the API theoretically supports 0.25, but as
|
---|
| 216 | * soon as you touch any built-up area, that kind of bounding box will download forever
|
---|
| 217 | * and then stop because it has more than 50k nodes.
|
---|
| 218 | */
|
---|
| 219 | final double bufferDist = panel.getDistance();
|
---|
| 220 | final double maxArea = panel.getArea() / 10000.0 / scale;
|
---|
| 221 | final double bufferY = bufferDist / 100000.0;
|
---|
| 222 | final double bufferX = bufferY / scale;
|
---|
| 223 | final int totalTicks = latcnt;
|
---|
| 224 | // guess if a progress bar might be useful.
|
---|
| 225 | final boolean displayProgress = totalTicks > 20_000 && bufferY < 0.01;
|
---|
| 226 |
|
---|
| 227 | class CalculateDownloadArea extends PleaseWaitRunnable {
|
---|
| 228 |
|
---|
| 229 | private final Path2D downloadPath = new Path2D.Double();
|
---|
[15363] | 230 | private final boolean newLayer;
|
---|
[14962] | 231 | private boolean cancel;
|
---|
| 232 | private int ticks;
|
---|
| 233 | private final Rectangle2D r = new Rectangle2D.Double();
|
---|
| 234 |
|
---|
[15363] | 235 | CalculateDownloadArea(boolean newLayer) {
|
---|
[14962] | 236 | super(tr("Calculating Download Area"), displayProgress ? null : NullProgressMonitor.INSTANCE, false);
|
---|
[15363] | 237 | this.newLayer = newLayer;
|
---|
[14962] | 238 | }
|
---|
| 239 |
|
---|
| 240 | @Override
|
---|
| 241 | protected void cancel() {
|
---|
| 242 | cancel = true;
|
---|
| 243 | }
|
---|
| 244 |
|
---|
| 245 | @Override
|
---|
| 246 | protected void finish() {
|
---|
| 247 | // Do nothing
|
---|
| 248 | }
|
---|
| 249 |
|
---|
| 250 | @Override
|
---|
| 251 | protected void afterFinish() {
|
---|
| 252 | if (cancel) {
|
---|
| 253 | return;
|
---|
| 254 | }
|
---|
| 255 | confirmAndDownloadAreas(new Area(downloadPath), maxArea, panel.isDownloadOsmData(), panel.isDownloadGpxData(),
|
---|
[15363] | 256 | confirmTitle, newLayer);
|
---|
[14962] | 257 | }
|
---|
| 258 |
|
---|
| 259 | /**
|
---|
| 260 | * increase tick count by one, report progress every 100 ticks
|
---|
| 261 | */
|
---|
| 262 | private void tick() {
|
---|
| 263 | ticks++;
|
---|
| 264 | if (ticks % 100 == 0) {
|
---|
| 265 | progressMonitor.worked(100);
|
---|
| 266 | }
|
---|
| 267 | }
|
---|
| 268 |
|
---|
| 269 | /**
|
---|
| 270 | * calculate area enclosing a single point
|
---|
| 271 | */
|
---|
| 272 | private void calcAreaForWayPoint(LatLon c) {
|
---|
| 273 | r.setRect(c.lon() - bufferX, c.lat() - bufferY, 2 * bufferX, 2 * bufferY);
|
---|
| 274 | downloadPath.append(r, false);
|
---|
| 275 | }
|
---|
| 276 |
|
---|
| 277 | @Override
|
---|
| 278 | protected void realRun() {
|
---|
| 279 | progressMonitor.setTicksCount(totalTicks);
|
---|
| 280 | PathIterator pit = alongPath.getPathIterator(null);
|
---|
| 281 | double[] res = new double[6];
|
---|
| 282 | LatLon previous = null;
|
---|
| 283 | while (!pit.isDone()) {
|
---|
| 284 | int type = pit.currentSegment(res);
|
---|
| 285 | LatLon c = new LatLon(res[1], res[0]);
|
---|
| 286 | if (type == PathIterator.SEG_LINETO) {
|
---|
| 287 | tick();
|
---|
| 288 | for (LatLon d : calcBetweenPoints(previous, c, bufferDist)) {
|
---|
| 289 | calcAreaForWayPoint(d);
|
---|
| 290 | }
|
---|
| 291 | previous = c;
|
---|
| 292 | } else if (type == PathIterator.SEG_MOVETO) {
|
---|
| 293 | previous = c;
|
---|
| 294 | tick();
|
---|
| 295 | calcAreaForWayPoint(c);
|
---|
| 296 | }
|
---|
| 297 | pit.next();
|
---|
| 298 | }
|
---|
| 299 | }
|
---|
| 300 | }
|
---|
| 301 |
|
---|
[15363] | 302 | return new CalculateDownloadArea(newLayer);
|
---|
[14962] | 303 | }
|
---|
| 304 |
|
---|
| 305 | @Override
|
---|
| 306 | public void actionPerformed(ActionEvent e) {
|
---|
| 307 | PleaseWaitRunnable task = createTask();
|
---|
| 308 | if (task != null) {
|
---|
| 309 | MainApplication.worker.submit(task);
|
---|
| 310 | }
|
---|
| 311 | }
|
---|
[7937] | 312 | }
|
---|