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