StreetsideData.java
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.streetside;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import org.apache.commons.jcs.access.CacheAccess;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.plugins.streetside.cache.CacheUtils;
import org.openstreetmap.josm.plugins.streetside.cache.Caches;
import org.openstreetmap.josm.plugins.streetside.gui.StreetsideMainDialog;
import org.openstreetmap.josm.plugins.streetside.gui.StreetsideViewerDialog;
import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoPanel;
import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
/**
* Database class for all the {@link StreetsideAbstractImage} objects.
*
* @author nokutu
* @author renerr18 (extended for Streetside)
* @see StreetsideAbstractImage
* @see StreetsideSequence
*/
public class StreetsideData {
private final Set<StreetsideAbstractImage> images = ConcurrentHashMap.newKeySet();
/**
* The image currently selected, this is the one being shown.
*/
private StreetsideAbstractImage selectedImage;
/**
* The image under the cursor.
*/
private StreetsideAbstractImage highlightedImage;
/**
* All the images selected, can be more than one.
*/
private final Set<StreetsideAbstractImage> multiSelectedImages = ConcurrentHashMap.newKeySet();
/**
* Listeners of the class.
*/
private final List<StreetsideDataListener> listeners = new CopyOnWriteArrayList<>();
/**
* The bounds of the areas for which the pictures have been downloaded.
*/
private final List<Bounds> bounds;
/**
* Creates a new object and adds the initial set of listeners.
*/
protected StreetsideData() {
selectedImage = null;
bounds = new CopyOnWriteArrayList<>();
// Adds the basic set of listeners.
Arrays.stream(StreetsidePlugin.getStreetsideDataListeners()).forEach(this::addListener);
if (Main.main != null) {
addListener(StreetsideMainDialog.getInstance());
addListener(ImageInfoPanel.getInstance());
addListener(StreetsideViewerDialog.getInstance().getStreetsideViewerPanel());
}
}
/**
* Adds an StreetsideImage to the object, and then repaints mapView.
*
* @param image The image to be added.
*/
public void add(StreetsideAbstractImage image) {
add(image, true);
}
/**
* Adds a StreetsideImage to the object, but doesn't repaint mapView. This is
* needed for concurrency.
*
* @param image The image to be added.
* @param update Whether the map must be updated or not
* (updates are currently unsupported by Streetside).
*/
public void add(StreetsideAbstractImage image, boolean update) {
images.add(image);
if (update) {
StreetsideLayer.invalidateInstance();
}
fireImagesAdded();
}
/**
* Adds a set of StreetsideImages to the object, and then repaints mapView.
*
* @param images The set of images to be added.
*/
public void addAll(Collection<? extends StreetsideAbstractImage> images) {
addAll(images, true);
}
/**
* Adds a set of {link StreetsideAbstractImage} objects to this object.
*
* @param newImages The set of images to be added.
* @param update Whether the map must be updated or not.
*/
public void addAll(Collection<? extends StreetsideAbstractImage> newImages, boolean update) {
images.addAll(newImages);
if (update) {
StreetsideLayer.invalidateInstance();
}
fireImagesAdded();
}
/**
* Adds a new listener.
*
* @param lis Listener to be added.
*/
public final void addListener(final StreetsideDataListener lis) {
listeners.add(lis);
}
/**
* Adds a {@link StreetsideImage} object to the list of selected images, (when
* ctrl + click)
*
* @param image The {@link StreetsideImage} object to be added.
*/
public void addMultiSelectedImage(final StreetsideAbstractImage image) {
if (!multiSelectedImages.contains(image)) {
if (getSelectedImage() == null) {
this.setSelectedImage(image);
} else {
multiSelectedImages.add(image);
}
}
StreetsideLayer.invalidateInstance();
}
/**
* Adds a set of {@code StreetsideAbstractImage} objects to the list of
* selected images.
*
* @param images A {@link Collection} object containing the set of images to be added.
*/
public void addMultiSelectedImage(Collection<StreetsideAbstractImage> images) {
images.stream().filter(image -> !multiSelectedImages.contains(image)).forEach(image -> {
if (getSelectedImage() == null) {
this.setSelectedImage(image);
} else {
multiSelectedImages.add(image);
}
});
StreetsideLayer.invalidateInstance();
}
public List<Bounds> getBounds() {
return bounds;
}
/**
* Removes a listener.
*
* @param lis Listener to be removed.
*/
public void removeListener(StreetsideDataListener lis) {
listeners.remove(lis);
}
/**
* Highlights the image under the cursor.
*
* @param image The image under the cursor.
*/
public void setHighlightedImage(StreetsideAbstractImage image) {
highlightedImage = image;
}
/**
* Returns the image under the mouse cursor.
*
* @return The image under the mouse cursor.
*/
public StreetsideAbstractImage getHighlightedImage() {
return highlightedImage;
}
/**
* Returns a Set containing all images.
*
* @return A Set object containing all images.
*/
public Set<StreetsideAbstractImage> getImages() {
return images;
}
/**
* Returns a Set of all sequences, that the images are part of.
* @return all sequences that are contained in the Streetside data
*/
public Set<StreetsideSequence> getSequences() {
return images.stream().map(StreetsideAbstractImage::getSequence).collect(Collectors.toSet());
}
/**
* Returns the StreetsideImage object that is currently selected.
*
* @return The selected StreetsideImage object.
*/
public StreetsideAbstractImage getSelectedImage() {
return selectedImage;
}
private void fireImagesAdded() {
listeners.stream().filter(Objects::nonNull).forEach(StreetsideDataListener::imagesAdded);
}
/**
* If the selected StreetsideImage is part of a StreetsideSequence then the
* following visible StreetsideImage is selected. In case there is none, does
* nothing.
*
* @throws IllegalStateException if the selected image is null or the selected image doesn't
* belong to a sequence.
*/
public void selectNext() {
selectNext(StreetsideProperties.MOVE_TO_IMG.get());
}
/**
* If the selected StreetsideImage is part of a StreetsideSequence then the
* following visible StreetsideImage is selected. In case there is none, does
* nothing.
*
* @param moveToPicture True if the view must me moved to the next picture.
* @throws IllegalStateException if the selected image is null or the selected image doesn't
* belong to a sequence.
*/
public void selectNext(boolean moveToPicture) {
if (getSelectedImage() == null) {
throw new IllegalStateException();
}
if (getSelectedImage().getSequence() == null) {
throw new IllegalStateException();
}
StreetsideAbstractImage tempImage = selectedImage;
while (tempImage.next() != null) {
tempImage = tempImage.next();
if (tempImage.isVisible()) {
setSelectedImage(tempImage, moveToPicture);
break;
}
}
}
/**
* If the selected StreetsideImage is part of a StreetsideSequence then the
* previous visible StreetsideImage is selected. In case there is none, does
* nothing.
*
* @throws IllegalStateException if the selected image is null or the selected image doesn't
* belong to a sequence.
*/
public void selectPrevious() {
selectPrevious(StreetsideProperties.MOVE_TO_IMG.get());
}
/**
* If the selected StreetsideImage is part of a StreetsideSequence then the
* previous visible StreetsideImage is selected. In case there is none, does
* nothing. * @throws IllegalStateException if the selected image is null or
* the selected image doesn't belong to a sequence.
*
* @param moveToPicture True if the view must me moved to the previous picture.
* @throws IllegalStateException if the selected image is null or the selected image doesn't
* belong to a sequence.
*/
public void selectPrevious(boolean moveToPicture) {
if (getSelectedImage() == null) {
throw new IllegalStateException();
}
if (getSelectedImage().getSequence() == null) {
throw new IllegalStateException();
}
StreetsideAbstractImage tempImage = selectedImage;
while (tempImage.previous() != null) {
tempImage = tempImage.previous();
if (tempImage.isVisible()) {
setSelectedImage(tempImage, moveToPicture);
break;
}
}
}
/**
* Selects a new image.If the user does ctrl + click, this isn't triggered.
*
* @param image The StreetsideImage which is going to be selected
*/
public void setSelectedImage(StreetsideAbstractImage image) {
setSelectedImage(image, false);
}
/**
* Selects a new image.If the user does ctrl+click, this isn't triggered. You
* can choose whether to center the view on the new image or not.
*
* @param image The {@link StreetsideImage} which is going to be selected.
* @param zoom True if the view must be centered on the image; false otherwise.
*/
public void setSelectedImage(StreetsideAbstractImage image, boolean zoom) {
StreetsideAbstractImage oldImage = selectedImage;
selectedImage = image;
multiSelectedImages.clear();
final MapView mv = StreetsidePlugin.getMapView();
if (image != null) {
multiSelectedImages.add(image);
if (mv != null && image instanceof StreetsideImage) {
StreetsideImage streetsideImage = (StreetsideImage) image;
// Downloading thumbnails of surrounding pictures.
StreetsideData.downloadSurroundingImages(streetsideImage);
}
}
if (mv != null && zoom && selectedImage != null) {
mv.zoomTo(selectedImage.getMovingLatLon());
}
fireSelectedImageChanged(oldImage, selectedImage);
StreetsideLayer.invalidateInstance();
}
/**
* Downloads surrounding images of this mapillary image in background threads
* @param streetsideImage the image for which the surrounding images should be downloaded
*/
private static void downloadSurroundingImages (StreetsideImage streetsideImage) {
MainApplication.worker.execute(() -> {
final int prefetchCount = StreetsideProperties.PRE_FETCH_IMAGE_COUNT.get();
CacheAccess <String, BufferedImageCacheEntry> imageCache = Caches.ImageCache.getInstance().getCache();
StreetsideAbstractImage nextImage = streetsideImage.next();
StreetsideAbstractImage prevImage = streetsideImage.previous();
for (int i = 0; i < prefetchCount; i++) {
if (nextImage != null) {
if (nextImage instanceof StreetsideImage &&
imageCache.get(((StreetsideImage) nextImage).getId()) == null) {
CacheUtils.downloadPicture((StreetsideImage) nextImage);
}
nextImage = nextImage.next();
}
if (prevImage != null) {
if (prevImage instanceof StreetsideImage &&
imageCache.get(((StreetsideImage) prevImage).getId()) == null) {
CacheUtils.downloadPicture((StreetsideImage) prevImage);
}
prevImage = prevImage.previous();
}
}
});
}
private void fireSelectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
listeners.stream().filter(Objects::nonNull).forEach(lis -> lis.selectedImageChanged(oldImage, newImage));
}
/**
* Returns a List containing all {@code StreetsideAbstractImage} objects
* selected with ctrl + click.
*
* @return A List object containing all the images selected.
*/
public Set<StreetsideAbstractImage> getMultiSelectedImages() {
return multiSelectedImages;
}
/**
* Sets a new {@link Collection} object as the used set of images.
* Any images that are already present, are removed.
*
* @param newImages the new image list (previously set images are completely replaced)
*/
public void setImages(Collection<StreetsideAbstractImage> newImages) {
synchronized (this) {
images.clear();
images.addAll(newImages);
}
}
}