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}