001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins.streetside;
003
004import java.util.Arrays;
005import java.util.Collection;
006import java.util.List;
007import java.util.Objects;
008import java.util.Set;
009import java.util.concurrent.ConcurrentHashMap;
010import java.util.concurrent.CopyOnWriteArrayList;
011import java.util.stream.Collectors;
012
013import org.apache.commons.jcs.access.CacheAccess;
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.data.Bounds;
016import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
017import org.openstreetmap.josm.gui.MainApplication;
018import org.openstreetmap.josm.gui.MapView;
019import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
020import org.openstreetmap.josm.plugins.streetside.cache.Caches;
021import org.openstreetmap.josm.plugins.streetside.gui.StreetsideMainDialog;
022import org.openstreetmap.josm.plugins.streetside.gui.StreetsideViewerDialog;
023import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoPanel;
024import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
025
026/**
027 * Database class for all the {@link StreetsideAbstractImage} objects.
028 *
029 * @author nokutu
030 * @author renerr18 (extended for Streetside)
031 * @see StreetsideAbstractImage
032 * @see StreetsideSequence
033 */
034public class StreetsideData {
035  private final Set<StreetsideAbstractImage> images = ConcurrentHashMap.newKeySet();
036  /**
037   * The image currently selected, this is the one being shown.
038   */
039  private StreetsideAbstractImage selectedImage;
040  /**
041   * The image under the cursor.
042   */
043  private StreetsideAbstractImage highlightedImage;
044  /**
045   * All the images selected, can be more than one.
046   */
047  private final Set<StreetsideAbstractImage> multiSelectedImages = ConcurrentHashMap.newKeySet();
048  /**
049   * Listeners of the class.
050   */
051  private final List<StreetsideDataListener> listeners = new CopyOnWriteArrayList<>();
052  /**
053   * The bounds of the areas for which the pictures have been downloaded.
054   */
055  private final List<Bounds> bounds;
056
057  /**
058   * Creates a new object and adds the initial set of listeners.
059   */
060  protected StreetsideData() {
061    selectedImage = null;
062    bounds = new CopyOnWriteArrayList<>();
063
064  // Adds the basic set of listeners.
065  Arrays.stream(StreetsidePlugin.getStreetsideDataListeners()).forEach(this::addListener);
066    if (Main.main != null) {
067      addListener(StreetsideViewerDialog.getInstance().getStreetsideViewerPanel());
068      addListener(StreetsideMainDialog.getInstance());
069      addListener(ImageInfoPanel.getInstance());
070    }
071  }
072
073  /**
074   * Adds an StreetsideImage to the object, and then repaints mapView.
075   *
076   * @param image The image to be added.
077   */
078  public void add(StreetsideAbstractImage image) {
079    add(image, true);
080  }
081
082  /**
083   * Adds a StreetsideImage to the object, but doesn't repaint mapView. This is
084   * needed for concurrency.
085   *
086   * @param image  The image to be added.
087   * @param update Whether the map must be updated or not
088   *        (updates are currently unsupported by Streetside).
089   */
090  public void add(StreetsideAbstractImage image, boolean update) {
091        images.add(image);
092        if (update) {
093                StreetsideLayer.invalidateInstance();
094        }
095        fireImagesAdded();
096  }
097
098  /**
099   * Adds a set of StreetsideImages to the object, and then repaints mapView.
100   *
101   * @param images The set of images to be added.
102   */
103  public void addAll(Collection<? extends StreetsideAbstractImage> images) {
104    addAll(images, true);
105  }
106
107  /**
108   * Adds a set of {link StreetsideAbstractImage} objects to this object.
109   *
110   * @param newImages The set of images to be added.
111   * @param update Whether the map must be updated or not.
112   */
113  public void addAll(Collection<? extends StreetsideAbstractImage> newImages, boolean update) {
114    images.addAll(newImages);
115    if (update) {
116      StreetsideLayer.invalidateInstance();
117    }
118    fireImagesAdded();
119  }
120
121 /**
122   * Adds a new listener.
123   *
124   * @param lis Listener to be added.
125   */
126  public final void addListener(final StreetsideDataListener lis) {
127    listeners.add(lis);
128  }
129
130  /**
131   * Adds a {@link StreetsideImage} object to the list of selected images, (when
132   * ctrl + click)
133   *
134   * @param image The {@link StreetsideImage} object to be added.
135   */
136  public void addMultiSelectedImage(final StreetsideAbstractImage image) {
137    if (!multiSelectedImages.contains(image)) {
138      if (getSelectedImage() == null) {
139        this.setSelectedImage(image);
140      } else {
141        multiSelectedImages.add(image);
142      }
143    }
144    StreetsideLayer.invalidateInstance();
145  }
146
147  /**
148   * Adds a set of {@code StreetsideAbstractImage} objects to the list of
149   * selected images.
150   *
151   * @param images A {@link Collection} object containing the set of images to be added.
152   */
153  public void addMultiSelectedImage(Collection<StreetsideAbstractImage> images) {
154    images.stream().filter(image -> !multiSelectedImages.contains(image)).forEach(image -> {
155      if (getSelectedImage() == null) {
156        this.setSelectedImage(image);
157      } else {
158        multiSelectedImages.add(image);
159      }
160    });
161    StreetsideLayer.invalidateInstance();
162  }
163
164  public List<Bounds> getBounds() {
165    return bounds;
166  }
167
168  /**
169   * Removes a listener.
170   *
171   * @param lis Listener to be removed.
172   */
173  public void removeListener(StreetsideDataListener lis) {
174    listeners.remove(lis);
175  }
176
177  /**
178   * Highlights the image under the cursor.
179   *
180   * @param image The image under the cursor.
181   */
182  public void setHighlightedImage(StreetsideAbstractImage image) {
183    highlightedImage = image;
184  }
185
186  /**
187   * Returns the image under the mouse cursor.
188   *
189   * @return The image under the mouse cursor.
190   */
191  public StreetsideAbstractImage getHighlightedImage() {
192    return highlightedImage;
193  }
194
195  /**
196   * Returns a Set containing all images.
197   *
198   * @return A Set object containing all images.
199   */
200  public Set<StreetsideAbstractImage> getImages() {
201    return images;
202  }
203
204  /**
205   * Returns a Set of all sequences, that the images are part of.
206   * @return all sequences that are contained in the Streetside data
207   */
208  public Set<StreetsideSequence> getSequences() {
209    return images.stream().map(StreetsideAbstractImage::getSequence).collect(Collectors.toSet());
210  }
211
212  /**
213   * Returns the StreetsideImage object that is currently selected.
214   *
215   * @return The selected StreetsideImage object.
216   */
217  public StreetsideAbstractImage getSelectedImage() {
218    return selectedImage;
219  }
220
221  private void fireImagesAdded() {
222    listeners.stream().filter(Objects::nonNull).forEach(StreetsideDataListener::imagesAdded);
223  }
224
225  /**
226   * If the selected StreetsideImage is part of a StreetsideSequence then the
227   * following visible StreetsideImage is selected. In case there is none, does
228   * nothing.
229   *
230   * @throws IllegalStateException if the selected image is null or the selected image doesn't
231   *                               belong to a sequence.
232   */
233  public void selectNext() {
234    selectNext(StreetsideProperties.MOVE_TO_IMG.get());
235  }
236
237  /**
238   * If the selected StreetsideImage is part of a StreetsideSequence then the
239   * following visible StreetsideImage is selected. In case there is none, does
240   * nothing.
241   *
242   * @param moveToPicture True if the view must me moved to the next picture.
243   * @throws IllegalStateException if the selected image is null or the selected image doesn't
244   *                               belong to a sequence.
245   */
246  public void selectNext(boolean moveToPicture) {
247    if (getSelectedImage() == null) {
248                throw new IllegalStateException();
249        }
250    if (getSelectedImage().getSequence() == null) {
251                throw new IllegalStateException();
252        }
253    StreetsideAbstractImage tempImage = selectedImage;
254    while (tempImage.next() != null) {
255      tempImage = tempImage.next();
256      if (tempImage.isVisible()) {
257        setSelectedImage(tempImage, moveToPicture);
258        break;
259      }
260    }
261  }
262
263  /**
264   * If the selected StreetsideImage is part of a StreetsideSequence then the
265   * previous visible StreetsideImage is selected. In case there is none, does
266   * nothing.
267   *
268   * @throws IllegalStateException if the selected image is null or the selected image doesn't
269   *                               belong to a sequence.
270   */
271  public void selectPrevious() {
272    selectPrevious(StreetsideProperties.MOVE_TO_IMG.get());
273  }
274
275  /**
276   * If the selected StreetsideImage is part of a StreetsideSequence then the
277   * previous visible StreetsideImage is selected. In case there is none, does
278   * nothing. * @throws IllegalStateException if the selected image is null or
279   * the selected image doesn't belong to a sequence.
280   *
281   * @param moveToPicture True if the view must me moved to the previous picture.
282   * @throws IllegalStateException if the selected image is null or the selected image doesn't
283   *                               belong to a sequence.
284   */
285  public void selectPrevious(boolean moveToPicture) {
286    if (getSelectedImage() == null) {
287                throw new IllegalStateException();
288        }
289    if (getSelectedImage().getSequence() == null) {
290                throw new IllegalStateException();
291        }
292    StreetsideAbstractImage tempImage = selectedImage;
293    while (tempImage.previous() != null) {
294      tempImage = tempImage.previous();
295      if (tempImage.isVisible()) {
296        setSelectedImage(tempImage, moveToPicture);
297        break;
298      }
299    }
300  }
301
302  /**
303   * Selects a new image.If the user does ctrl + click, this isn't triggered.
304   *
305   * @param image The StreetsideImage which is going to be selected
306   */
307  public void setSelectedImage(StreetsideAbstractImage image) {
308    setSelectedImage(image, false);
309  }
310
311  /**
312   * Selects a new image.If the user does ctrl+click, this isn't triggered. You
313   * can choose whether to center the view on the new image or not.
314   *
315   * @param image The {@link StreetsideImage} which is going to be selected.
316   * @param zoom  True if the view must be centered on the image; false otherwise.
317   */
318  public void setSelectedImage(StreetsideAbstractImage image, boolean zoom) {
319    StreetsideAbstractImage oldImage = selectedImage;
320    selectedImage = image;
321    multiSelectedImages.clear();
322    final MapView mv = StreetsidePlugin.getMapView();
323    if (image != null) {
324      multiSelectedImages.add(image);
325      if (mv != null && image instanceof StreetsideImage) {
326        StreetsideImage streetsideImage = (StreetsideImage) image;
327
328        // Downloading thumbnails of surrounding pictures.
329        downloadSurroundingImages(streetsideImage);
330      }
331    }
332    if (mv != null && zoom && selectedImage != null) {
333      mv.zoomTo(selectedImage.getMovingLatLon());
334    }
335    fireSelectedImageChanged(oldImage, selectedImage);
336    StreetsideLayer.invalidateInstance();
337  }
338
339  /**
340   * Downloads surrounding images of this mapillary image in background threads
341   * @param streetsideImage the image for which the surrounding images should be downloaded
342   */
343  private static void downloadSurroundingImages (StreetsideImage streetsideImage) {
344    MainApplication.worker.execute(() -> {
345      final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
346      CacheAccess <String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
347
348      StreetsideAbstractImage nextImage = streetsideImage.next();
349      StreetsideAbstractImage prevImage = streetsideImage.previous();
350
351      for (int i = 0; i < prefetchCount; i++) {
352        if (nextImage != null) {
353          if (nextImage instanceof StreetsideImage &&
354            imageCache.get(((StreetsideImage) nextImage).getId()) == null) {
355            CacheUtils.downloadPicture((StreetsideImage) nextImage);
356          }
357          nextImage = nextImage.next();
358        }
359        if (prevImage != null) {
360          if (prevImage instanceof StreetsideImage &&
361            imageCache.get(((StreetsideImage) prevImage).getId()) == null) {
362            CacheUtils.downloadPicture((StreetsideImage) prevImage);
363          }
364          prevImage = prevImage.previous();
365        }
366      }
367    });
368  }
369
370  /**
371   * Downloads surrounding images of this mapillary image in background threads
372   * @param streetsideImage the image for which the surrounding images should be downloaded
373   */
374  public static void downloadSurroundingCubemaps(StreetsideImage streetsideImage) {
375      MainApplication.worker.execute(() -> {
376        final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
377        CacheAccess<String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
378
379        StreetsideAbstractImage nextImage = streetsideImage.next();
380        StreetsideAbstractImage prevImage = streetsideImage.previous();
381
382        for (int i = 0; i < prefetchCount; i++) {
383          if (nextImage != null) {
384            if (nextImage instanceof StreetsideImage && imageCache.get(((StreetsideImage) nextImage).getId()) == null) {
385              CacheUtils.downloadCubemap((StreetsideImage) nextImage);
386            }
387            nextImage = nextImage.next();
388          }
389          if (prevImage != null) {
390            if (prevImage instanceof StreetsideImage && imageCache.get(((StreetsideImage) prevImage).getId()) == null) {
391              CacheUtils.downloadCubemap((StreetsideImage) prevImage);
392            }
393            prevImage = prevImage.previous();
394          }
395        }
396      });
397  }
398
399  private void fireSelectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
400        listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.selectedImageChanged(oldImage, newImage));
401  }
402
403  /**
404   * Returns a List containing all {@code StreetsideAbstractImage} objects
405   * selected with ctrl + click.
406   *
407   * @return A List object containing all the images selected.
408   */
409  public Set<StreetsideAbstractImage> getMultiSelectedImages() {
410    return multiSelectedImages;
411  }
412
413  /**
414   * Sets a new {@link Collection} object as the used set of images.
415   * Any images that are already present, are removed.
416   *
417   * @param newImages the new image list (previously set images are completely replaced)
418   */
419  public void setImages(Collection<StreetsideAbstractImage> newImages) {
420    synchronized (this) {
421      images.clear();
422      images.addAll(newImages);
423    }
424  }
425}