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}