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}