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.Collections;
007import java.util.List;
008import java.util.Objects;
009import java.util.Set;
010import java.util.concurrent.ConcurrentHashMap;
011import java.util.concurrent.CopyOnWriteArrayList;
012import java.util.stream.Collectors;
013
014import org.apache.commons.jcs3.access.CacheAccess;
015import org.openstreetmap.josm.data.Bounds;
016import org.openstreetmap.josm.data.Data;
017import org.openstreetmap.josm.data.DataSource;
018import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
019import org.openstreetmap.josm.gui.MainApplication;
020import org.openstreetmap.josm.gui.MapView;
021import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
022import org.openstreetmap.josm.plugins.streetside.cache.Caches;
023import org.openstreetmap.josm.plugins.streetside.gui.StreetsideMainDialog;
024import org.openstreetmap.josm.plugins.streetside.gui.StreetsideViewerDialog;
025import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoPanel;
026import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
027
028/**
029 * Database class for all the {@link StreetsideAbstractImage} objects.
030 *
031 * @author nokutu
032 * @author renerr18 (extended for Streetside)
033 * @see StreetsideAbstractImage
034 * @see StreetsideSequence
035 */
036public class StreetsideData implements Data {
037  private final Set<StreetsideAbstractImage> images = ConcurrentHashMap.newKeySet();
038  /**
039   * The image currently selected, this is the one being shown.
040   */
041  private StreetsideAbstractImage selectedImage;
042  /**
043   * The image under the cursor.
044   */
045  private StreetsideAbstractImage highlightedImage;
046  /**
047   * All the images selected, can be more than one.
048   */
049  private final Set<StreetsideAbstractImage> multiSelectedImages = ConcurrentHashMap.newKeySet();
050  /**
051   * Listeners of the class.
052   */
053  private final List<StreetsideDataListener> listeners = new CopyOnWriteArrayList<>();
054  /**
055   * The bounds of the areas for which the pictures have been downloaded.
056   */
057  private final List<Bounds> bounds;
058
059  /**
060   * Creates a new object and adds the initial set of listeners.
061   */
062  protected StreetsideData() {
063    selectedImage = null;
064    bounds = new CopyOnWriteArrayList<>();
065
066  // Adds the basic set of listeners.
067  Arrays.stream(StreetsidePlugin.getStreetsideDataListeners()).forEach(this::addListener);
068      addListener(StreetsideViewerDialog.getInstance().getStreetsideViewerPanel());
069      addListener(StreetsideMainDialog.getInstance());
070      addListener(ImageInfoPanel.getInstance());
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    StreetsideAbstractImage tempImage = selectedImage;
248    if (selectedImage != null && selectedImage.getSequence() != null) {
249      while (tempImage.next() != null) {
250        tempImage = tempImage.next();
251        if (tempImage.isVisible()) {
252          setSelectedImage(tempImage, moveToPicture);
253          break;
254        }
255      }
256    }
257  }
258
259  /**
260   * If the selected StreetsideImage is part of a StreetsideSequence then the
261   * previous visible StreetsideImage is selected. In case there is none, does
262   * nothing.
263   *
264   * @throws IllegalStateException if the selected image is null or the selected image doesn't
265   *                               belong to a sequence.
266   */
267  public void selectPrevious() {
268    selectPrevious(StreetsideProperties.MOVE_TO_IMG.get());
269  }
270
271  /**
272   * If the selected StreetsideImage is part of a StreetsideSequence then the
273   * previous visible StreetsideImage is selected. In case there is none, does
274   * nothing. * @throws IllegalStateException if the selected image is null or
275   * the selected image doesn't belong to a sequence.
276   *
277   * @param moveToPicture True if the view must me moved to the previous picture.
278   * @throws IllegalStateException if the selected image is null or the selected image doesn't
279   *                               belong to a sequence.
280   */
281  public void selectPrevious(boolean moveToPicture) {
282    if (selectedImage != null && selectedImage.getSequence() != null) {
283      StreetsideAbstractImage tempImage = selectedImage;
284      while (tempImage.previous() != null) {
285        tempImage = tempImage.previous();
286        if (tempImage.isVisible()) {
287          setSelectedImage(tempImage, moveToPicture);
288          break;
289        }
290      }
291    }
292  }
293
294  /**
295   * Selects a new image.If the user does ctrl + click, this isn't triggered.
296   *
297   * @param image The StreetsideImage which is going to be selected
298   */
299  public void setSelectedImage(StreetsideAbstractImage image) {
300    setSelectedImage(image, false);
301  }
302
303  /**
304   * Selects a new image.If the user does ctrl+click, this isn't triggered. You
305   * can choose whether to center the view on the new image or not.
306   *
307   * @param image The {@link StreetsideImage} which is going to be selected.
308   * @param zoom  True if the view must be centered on the image; false otherwise.
309   */
310  public void setSelectedImage(StreetsideAbstractImage image, boolean zoom) {
311    StreetsideAbstractImage oldImage = selectedImage;
312    selectedImage = image;
313    multiSelectedImages.clear();
314    final MapView mv = StreetsidePlugin.getMapView();
315    if (image != null) {
316      multiSelectedImages.add(image);
317      if (mv != null && image instanceof StreetsideImage) {
318        StreetsideImage streetsideImage = (StreetsideImage) image;
319
320        // Downloading thumbnails of surrounding pictures.
321        downloadSurroundingImages(streetsideImage);
322      }
323    }
324    if (mv != null && zoom && selectedImage != null) {
325      mv.zoomTo(selectedImage.getMovingLatLon());
326    }
327    fireSelectedImageChanged(oldImage, selectedImage);
328    StreetsideLayer.invalidateInstance();
329  }
330
331  /**
332   * Downloads surrounding images of this mapillary image in background threads
333   * @param streetsideImage the image for which the surrounding images should be downloaded
334   */
335  private static void downloadSurroundingImages (StreetsideImage streetsideImage) {
336    MainApplication.worker.execute(() -> {
337      final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
338      CacheAccess <String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
339
340      StreetsideAbstractImage nextImage = streetsideImage.next();
341      StreetsideAbstractImage prevImage = streetsideImage.previous();
342
343      for (int i = 0; i < prefetchCount; i++) {
344        if (nextImage != null) {
345          if (nextImage instanceof StreetsideImage &&
346            imageCache.get(((StreetsideImage) nextImage).getId()) == null) {
347            CacheUtils.downloadPicture((StreetsideImage) nextImage);
348          }
349          nextImage = nextImage.next();
350        }
351        if (prevImage != null) {
352          if (prevImage instanceof StreetsideImage &&
353            imageCache.get(((StreetsideImage) prevImage).getId()) == null) {
354            CacheUtils.downloadPicture((StreetsideImage) prevImage);
355          }
356          prevImage = prevImage.previous();
357        }
358      }
359    });
360  }
361
362  /**
363   * Downloads surrounding images of this mapillary image in background threads
364   * @param streetsideImage the image for which the surrounding images should be downloaded
365   */
366  public static void downloadSurroundingCubemaps(StreetsideImage streetsideImage) {
367      MainApplication.worker.execute(() -> {
368        final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
369        CacheAccess<String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
370
371        StreetsideAbstractImage nextImage = streetsideImage.next();
372        StreetsideAbstractImage prevImage = streetsideImage.previous();
373
374        for (int i = 0; i < prefetchCount; i++) {
375          if (nextImage != null) {
376            if (nextImage instanceof StreetsideImage && imageCache.get(((StreetsideImage) nextImage).getId()) == null) {
377              CacheUtils.downloadCubemap((StreetsideImage) nextImage);
378            }
379            nextImage = nextImage.next();
380          }
381          if (prevImage != null) {
382            if (prevImage instanceof StreetsideImage && imageCache.get(((StreetsideImage) prevImage).getId()) == null) {
383              CacheUtils.downloadCubemap((StreetsideImage) prevImage);
384            }
385            prevImage = prevImage.previous();
386          }
387        }
388      });
389  }
390
391  private void fireSelectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
392        listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.selectedImageChanged(oldImage, newImage));
393  }
394
395  /**
396   * Returns a List containing all {@code StreetsideAbstractImage} objects
397   * selected with ctrl + click.
398   *
399   * @return A List object containing all the images selected.
400   */
401  public Set<StreetsideAbstractImage> getMultiSelectedImages() {
402    return multiSelectedImages;
403  }
404
405  /**
406   * Sets a new {@link Collection} object as the used set of images.
407   * Any images that are already present, are removed.
408   *
409   * @param newImages the new image list (previously set images are completely replaced)
410   */
411  public void setImages(Collection<StreetsideAbstractImage> newImages) {
412    synchronized (this) {
413      images.clear();
414      images.addAll(newImages);
415    }
416  }
417
418  @Override
419  public Collection<DataSource> getDataSources() {
420        return Collections.emptyList();
421  }
422}