    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    this.selectedImage = null;
    062    this.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(StreetsideMainDialog.getInstance());
    068      addListener(ImageInfoPanel.getInstance());
    069      addListener(StreetsideViewerDialog.getInstance().getStreetsideViewerPanel());
    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 (!this.multiSelectedImages.contains(image)) {
    138      if (this.getSelectedImage() == null) {
    139        this.setSelectedImage(image);
    140      } else {
    141        this.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 -> !this.multiSelectedImages.contains(image)).forEach(image -> {
    155      if (this.getSelectedImage() == null) {
    156        this.setSelectedImage(image);
    157      } else {
    158        this.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 this.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 = this.selectedImage;
    320    this.selectedImage = image;
    321    this.multiSelectedImages.clear();
    322    final MapView mv = StreetsidePlugin.getMapView();
    323    if (image != null) {
    324      this.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, this.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  private void fireSelectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
    371        listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.selectedImageChanged(oldImage, newImage));
    372  }
    373
    374  /**
    375   * Returns a List containing all {@code StreetsideAbstractImage} objects
    376   * selected with ctrl + click.
    377   *
    378   * @return A List object containing all the images selected.
    379   */
    380  public Set<StreetsideAbstractImage> getMultiSelectedImages() {
    381    return this.multiSelectedImages;
    382  }
    383
    384  /**
    385   * Sets a new {@link Collection} object as the used set of images.
    386   * Any images that are already present, are removed.
    387   *
    388   * @param newImages the new image list (previously set images are completely replaced)
    389   */
    390  public void setImages(Collection<StreetsideAbstractImage> newImages) {
    391    synchronized (this) {
    392      images.clear();
    393      images.addAll(newImages);
    394    }
    395  }
    396}