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}