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}