001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.GraphicsEnvironment; 008import java.awt.GridBagLayout; 009import java.awt.event.ActionEvent; 010import java.awt.geom.Area; 011import java.awt.geom.Path2D; 012import java.awt.geom.PathIterator; 013import java.awt.geom.Rectangle2D; 014import java.text.MessageFormat; 015import java.util.ArrayList; 016import java.util.Collection; 017import java.util.List; 018import java.util.concurrent.Future; 019 020import javax.swing.JLabel; 021import javax.swing.JOptionPane; 022import javax.swing.JPanel; 023 024import org.openstreetmap.josm.actions.downloadtasks.DownloadTaskList; 025import org.openstreetmap.josm.data.coor.LatLon; 026import org.openstreetmap.josm.data.osm.DataSet; 027import org.openstreetmap.josm.gui.MainApplication; 028import org.openstreetmap.josm.gui.PleaseWaitRunnable; 029import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongPanel; 030import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 031import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor; 032import org.openstreetmap.josm.spi.preferences.Config; 033import org.openstreetmap.josm.tools.GBC; 034import org.openstreetmap.josm.tools.Logging; 035import org.openstreetmap.josm.tools.Shortcut; 036import org.openstreetmap.josm.tools.Utils; 037 038/** 039 * Abstract superclass of DownloadAlongTrackAction and DownloadAlongWayAction 040 * @since 6054 041 */ 042public abstract class DownloadAlongAction extends JosmAction { 043 044 /** 045 * Sub classes must override this method. 046 * @return the task to start or null if nothing to do 047 */ 048 protected abstract PleaseWaitRunnable createTask(); 049 050 /** 051 * Constructs a new {@code DownloadAlongAction} 052 * @param name the action's text as displayed in the menu 053 * @param iconName the filename of the icon to use 054 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 055 * that html is not supported for menu actions on some platforms. 056 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 057 * do want a shortcut, remember you can always register it with group=none, so you 058 * won't be assigned a shortcut unless the user configures one. If you pass null here, 059 * the user CANNOT configure a shortcut for your action. 060 * @param registerInToolbar register this action for the toolbar preferences? 061 */ 062 protected DownloadAlongAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) { 063 super(name, iconName, tooltip, shortcut, registerInToolbar, null, false); 064 } 065 066 protected static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double maxArea) { 067 Area tmp = new Area(r); 068 // intersect with sought-after area 069 tmp.intersect(a); 070 if (tmp.isEmpty()) { 071 return; 072 } 073 Rectangle2D bounds = tmp.getBounds2D(); 074 if (bounds.getWidth() * bounds.getHeight() > maxArea) { 075 // the rectangle gets too large; split it and make recursive call. 076 Rectangle2D r1; 077 Rectangle2D r2; 078 if (bounds.getWidth() > bounds.getHeight()) { 079 // rectangles that are wider than high are split into a left and right half, 080 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth() / 2, bounds.getHeight()); 081 r2 = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 2, bounds.getY(), 082 bounds.getWidth() / 2, bounds.getHeight()); 083 } else { 084 // others into a top and bottom half. 085 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() / 2); 086 r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY() + bounds.getHeight() / 2, bounds.getWidth(), 087 bounds.getHeight() / 2); 088 } 089 addToDownload(tmp, r1, results, maxArea); 090 addToDownload(tmp, r2, results, maxArea); 091 } else { 092 DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 093 if (ds != null) { 094 double p = LatLon.MAX_SERVER_PRECISION; 095 LatLon min = new LatLon(bounds.getY()+p, bounds.getX()+p); 096 LatLon max = new LatLon(bounds.getY()+bounds.getHeight()-p, bounds.getX()+bounds.getWidth()-p); 097 if (ds.getDataSourceBounds().stream().anyMatch(current -> (current.contains(min) && current.contains(max)))) { 098 return; // skip this one, already downloaded 099 } 100 } 101 results.add(bounds); 102 } 103 } 104 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 116 * (upper and lower half), but we do not request the full upper and lower rectangle, only 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. 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 127 * @param newLayer Set to true if all areas should be put into a single new layer 128 */ 129 protected static void confirmAndDownloadAreas(Area a, double maxArea, boolean osmDownload, boolean gpxDownload, String title, 130 boolean newLayer) { 131 List<Rectangle2D> toDownload = new ArrayList<>(); 132 addToDownload(a, a.getBounds(), toDownload, maxArea); 133 if (toDownload.isEmpty()) { 134 return; 135 } 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 } 146 } 147 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Download data")); 148 final Future<?> future = new DownloadTaskList(Config.getPref().getBoolean("download.along.zoom-after-download")) 149 .download(newLayer, toDownload, osmDownload, gpxDownload, monitor); 150 waitFuture(future, monitor); 151 } 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) { 164 double d = p2.greatCircleDistance(p1) / bufferDist; 165 int nbNodes = (int) d; 166 if (Logging.isDebugEnabled()) { 167 Logging.debug(MessageFormat.format("{0} intermediate nodes to download between {1} and {2}", nbNodes, p2, p1)); 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 187 * @param newLayer Set to true if all areas should be put into a single new layer 188 * @return the task or null if canceled by user 189 */ 190 protected PleaseWaitRunnable createCalcTask(Path2D alongPath, DownloadAlongPanel panel, String confirmTitle, boolean newLayer) { 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(); 230 private final boolean newLayer; 231 private boolean cancel; 232 private int ticks; 233 private final Rectangle2D r = new Rectangle2D.Double(); 234 235 CalculateDownloadArea(boolean newLayer) { 236 super(tr("Calculating Download Area"), displayProgress ? null : NullProgressMonitor.INSTANCE, false); 237 this.newLayer = newLayer; 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(), 256 confirmTitle, newLayer); 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 302 return new CalculateDownloadArea(newLayer); 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 } 312}