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