001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins.streetside.io.download;
003
004import java.util.concurrent.ArrayBlockingQueue;
005import java.util.concurrent.ThreadPoolExecutor;
006import java.util.concurrent.TimeUnit;
007
008import org.apache.log4j.Logger;
009import org.openstreetmap.josm.data.Bounds;
010import org.openstreetmap.josm.data.coor.LatLon;
011import org.openstreetmap.josm.gui.MainApplication;
012import org.openstreetmap.josm.gui.Notification;
013import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
014import org.openstreetmap.josm.plugins.streetside.StreetsidePlugin;
015import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
016import org.openstreetmap.josm.tools.I18n;
017
018/**
019 * Class that concentrates all the ways of downloading of the plugin. All the
020 * download petitions will be managed one by one.
021 *
022 * @author nokutu
023 */
024public final class StreetsideDownloader {
025
026  final static Logger logger = Logger.getLogger(StreetsideDownloader.class);
027
028  /** Possible download modes. */
029  public enum DOWNLOAD_MODE {
030    VISIBLE_AREA("visibleArea", I18n.tr("everything in the visible area")),
031
032    OSM_AREA("osmArea", I18n.tr("areas with downloaded OSM-data")),
033
034    MANUAL_ONLY("manualOnly", I18n.tr("only when manually requested"));
035
036    public final static DOWNLOAD_MODE DEFAULT = OSM_AREA;
037
038    private final String prefId;
039    private final String label;
040
041    DOWNLOAD_MODE(String prefId, String label) {
042      this.prefId = prefId;
043      this.label = label;
044    }
045
046    /**
047     * @return the ID that is used to represent this download mode in the JOSM preferences
048     */
049    public String getPrefId() {
050      return prefId;
051    }
052
053    /**
054     * @return the (internationalized) label describing this download mode
055     */
056    public String getLabel() {
057      return label;
058    }
059
060    public static DOWNLOAD_MODE fromPrefId(String prefId) {
061      for (DOWNLOAD_MODE mode : DOWNLOAD_MODE.values()) {
062        if (mode.getPrefId().equals(prefId)) {
063          return mode;
064        }
065      }
066      return DEFAULT;
067    }
068
069    public static DOWNLOAD_MODE fromLabel(String label) {
070      for (DOWNLOAD_MODE mode : DOWNLOAD_MODE.values()) {
071        if (mode.getLabel().equals(label)) {
072          return mode;
073        }
074      }
075      return DEFAULT;
076    }
077  }
078
079  /** Max area to be downloaded */
080  private static final double MAX_AREA = StreetsideProperties.MAX_DOWNLOAD_AREA.get();
081
082  /** Executor that will run the petitions. */
083  private static ThreadPoolExecutor executor = new ThreadPoolExecutor(
084    3, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.DiscardPolicy());
085
086  /**
087   * Indicates whether the last download request has been rejected because it requested an area that was too big.
088   * Iff true, the last download has been rejected, if false, it was executed.
089   */
090  private static boolean stoppedDownload;
091
092  private StreetsideDownloader() {
093    // Private constructor to avoid instantiation
094  }
095
096  /**
097   * Gets all the images in a square. It downloads all the images of all the
098   * sequences that pass through the given rectangle.
099   *
100   * @param minLatLon The minimum latitude and longitude of the rectangle.
101   * @param maxLatLon The maximum latitude and longitude of the rectangle
102   */
103  public static void getImages(LatLon minLatLon, LatLon maxLatLon) {
104    if (minLatLon == null || maxLatLon == null) {
105      throw new IllegalArgumentException();
106    }
107    getImages(new Bounds(minLatLon, maxLatLon));
108  }
109
110  /**
111   * Gets the images within the given bounds.
112   *
113   * @param bounds A {@link Bounds} object containing the area to be downloaded.
114   */
115  public static void getImages(Bounds bounds) {
116    run(new StreetsideSquareDownloadRunnable(bounds));
117  }
118
119  /**
120   * Returns the current download mode.
121   *
122   * @return the currently enabled {@link DOWNLOAD_MODE}
123   */
124  public static DOWNLOAD_MODE getMode() {
125    return DOWNLOAD_MODE.fromPrefId(StreetsideProperties.DOWNLOAD_MODE.get());
126  }
127
128  private static void run(Runnable t) {
129    executor.execute(t);
130  }
131
132  /**
133   * If some part of the current view has not been downloaded, it is downloaded.
134   */
135  public static void downloadVisibleArea() {
136    Bounds view = MainApplication.getMap().mapView.getRealBounds();
137    if (isAreaTooBig(view.getArea())) {
138      return;
139    }
140    if (isViewDownloaded(view)) {
141      return;
142    }
143    StreetsideLayer.getInstance().getData().getBounds().add(view);
144    getImages(view);
145  }
146
147  private static boolean isViewDownloaded(Bounds view) {
148    int n = 15;
149    boolean[][] inside = new boolean[n][n];
150    for (int i = 0; i < n; i++) {
151      for (int j = 0; j < n; j++) {
152        if (isInBounds(new LatLon(view.getMinLat()
153          + (view.getMaxLat() - view.getMinLat()) * ((double) i / n),
154          view.getMinLon() + (view.getMaxLon() - view.getMinLon())
155            * ((double) j / n)))) {
156          inside[i][j] = true;
157        }
158      }
159    }
160    for (int i = 0; i < n; i++) {
161      for (int j = 0; j < n; j++) {
162        if (!inside[i][j])
163          return false;
164      }
165    }
166    return true;
167  }
168
169  /**
170   * Checks if the given {@link LatLon} object lies inside the bounds of the
171   * image.
172   *
173   * @param latlon The coordinates to check.
174   *
175   * @return true if it lies inside the bounds; false otherwise;
176   */
177  private static boolean isInBounds(LatLon latlon) {
178    return StreetsideLayer.getInstance().getData().getBounds().parallelStream().anyMatch(b -> b.contains(latlon));
179  }
180
181  /**
182   * Downloads all images of the area covered by the OSM data.
183   */
184  public static void downloadOSMArea() {
185    if (MainApplication.getLayerManager().getEditLayer() == null) {
186      return;
187    }
188    if (isAreaTooBig(MainApplication.getLayerManager().getEditLayer().data.getDataSourceBounds().parallelStream().map(Bounds::getArea).reduce(0.0, Double::sum))) {
189      return;
190    }
191    MainApplication.getLayerManager().getEditLayer().data.getDataSourceBounds().stream().filter(bounds -> !StreetsideLayer.getInstance().getData().getBounds().contains(bounds)).forEach(bounds -> {
192      StreetsideLayer.getInstance().getData().getBounds().add(bounds);
193      StreetsideDownloader.getImages(bounds.getMin(), bounds.getMax());
194    });
195  }
196
197  /**
198   * Checks if the area for which Streetside images should be downloaded is too big. This means that probably
199   * lots of Streetside images are going to be downloaded, slowing down the
200   * program too much. A notification is shown when the download has stopped or continued.
201   * @param area area to check
202   * @return {@code true} if the area is too big
203   */
204  private static boolean isAreaTooBig(final double area) {
205    final boolean tooBig = area > MAX_AREA;
206    if (!stoppedDownload && tooBig) {
207      new Notification(
208        I18n.tr("The Streetside layer has stopped downloading images, because the requested area is too big!") + (
209          getMode() == DOWNLOAD_MODE.VISIBLE_AREA
210          ? "\n"+I18n.tr("To solve this problem, you could zoom in and load a smaller area of the map.")
211          : (getMode() == DOWNLOAD_MODE.OSM_AREA ? "\n"+I18n.tr("To solve this problem, you could switch to download mode ''{0}'' and load Streetside images for a smaller portion of the map.", DOWNLOAD_MODE.MANUAL_ONLY): "")
212        )
213      ).setIcon(StreetsidePlugin.LOGO.get()).setDuration(Notification.TIME_LONG).show();
214    }
215    if (stoppedDownload && !tooBig) {
216      new Notification("The Streetside layer now continues to download images…").setIcon(StreetsidePlugin.LOGO.get()).show();
217    }
218    stoppedDownload = tooBig;
219    return tooBig;
220  }
221
222  /**
223   * Stops all running threads.
224   */
225  public static void stopAll() {
226    executor.shutdownNow();
227    try {
228      executor.awaitTermination(30, TimeUnit.SECONDS);
229    } catch (InterruptedException e) {
230      logger.error(e);
231    }
232    executor = new ThreadPoolExecutor(3, 5, 100, TimeUnit.SECONDS,
233      new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.DiscardPolicy());
234  }
235}