001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Collections; 007import java.util.List; 008import java.util.stream.Collectors; 009 010import org.openstreetmap.josm.data.coor.LatLon; 011import org.openstreetmap.josm.data.gpx.GpxImageEntry; 012import org.openstreetmap.josm.data.osm.QuadBuckets; 013import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry; 014import org.openstreetmap.josm.tools.ListenerList; 015 016/** 017 * Class to hold {@link ImageEntry} and the current selection 018 * @since 14590 019 */ 020public class ImageData implements Data { 021 /** 022 * A listener that is informed when the current selection change 023 */ 024 public interface ImageDataUpdateListener { 025 /** 026 * Called when the data change 027 * @param data the image data 028 */ 029 void imageDataUpdated(ImageData data); 030 031 /** 032 * Called when the selection change 033 * @param data the image data 034 */ 035 void selectedImageChanged(ImageData data); 036 } 037 038 private final List<ImageEntry> data; 039 040 private final List<Integer> selectedImagesIndex = new ArrayList<>(); 041 042 private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create(); 043 private final QuadBuckets<ImageEntry> geoImages = new QuadBuckets<>(); 044 045 /** 046 * Construct a new image container without images 047 */ 048 public ImageData() { 049 this(null); 050 } 051 052 /** 053 * Construct a new image container with a list of images 054 * @param data the list of {@link ImageEntry} 055 */ 056 public ImageData(List<ImageEntry> data) { 057 if (data != null) { 058 Collections.sort(data); 059 this.data = data; 060 this.data.forEach(image -> image.setDataSet(this)); 061 } else { 062 this.data = new ArrayList<>(); 063 } 064 this.geoImages.addAll(this.data); 065 selectedImagesIndex.add(-1); 066 } 067 068 /** 069 * Returns the images 070 * @return the images 071 */ 072 public List<ImageEntry> getImages() { 073 return data; 074 } 075 076 /** 077 * Determines if one image has modified GPS data. 078 * @return {@code true} if data has been modified; {@code false}, otherwise 079 */ 080 public boolean isModified() { 081 return data.stream().anyMatch(GpxImageEntry::hasNewGpsData); 082 } 083 084 /** 085 * Merge 2 ImageData 086 * @param otherData {@link ImageData} to merge 087 */ 088 public void mergeFrom(ImageData otherData) { 089 data.addAll(otherData.getImages()); 090 this.geoImages.addAll(otherData.getImages()); 091 Collections.sort(data); 092 093 final ImageEntry selected = otherData.getSelectedImage(); 094 095 // Suppress the double photos. 096 if (data.size() > 1) { 097 ImageEntry prev = data.get(data.size() - 1); 098 for (int i = data.size() - 2; i >= 0; i--) { 099 ImageEntry cur = data.get(i); 100 if (cur.getFile().equals(prev.getFile())) { 101 data.remove(i); 102 } else { 103 prev = cur; 104 } 105 } 106 } 107 if (selected != null) { 108 setSelectedImageIndex(data.indexOf(selected)); 109 } 110 } 111 112 /** 113 * Return the first currently selected image 114 * @return the first selected image as {@link ImageEntry} or null 115 * @see #getSelectedImages 116 */ 117 public ImageEntry getSelectedImage() { 118 int selectedImageIndex = selectedImagesIndex.isEmpty() ? -1 : selectedImagesIndex.get(0); 119 if (selectedImageIndex > -1) { 120 return data.get(selectedImageIndex); 121 } 122 return null; 123 } 124 125 /** 126 * Return the current selected images 127 * @return the selected images as list {@link ImageEntry} 128 * @since 15333 129 */ 130 public List<ImageEntry> getSelectedImages() { 131 return selectedImagesIndex.stream().filter(i -> i > -1 && i < data.size()).map(data::get).collect(Collectors.toList()); 132 } 133 134 /** 135 * Get the first image on the layer 136 * @return The first image 137 * @since 18246 138 */ 139 public ImageEntry getFirstImage() { 140 if (!this.data.isEmpty()) { 141 return this.data.get(0); 142 } 143 return null; 144 } 145 146 /** 147 * Select the first image of the sequence 148 * @deprecated Use {@link #getFirstImage()} in conjunction with {@link #setSelectedImage} 149 */ 150 @Deprecated 151 public void selectFirstImage() { 152 if (!data.isEmpty()) { 153 setSelectedImageIndex(0); 154 } 155 } 156 157 /** 158 * Get the last image in the layer 159 * @return The last image 160 * @since 18246 161 */ 162 public ImageEntry getLastImage() { 163 if (!this.data.isEmpty()) { 164 return this.data.get(this.data.size() - 1); 165 } 166 return null; 167 } 168 169 /** 170 * Select the last image of the sequence 171 * @deprecated Use {@link #getLastImage()} with {@link #setSelectedImage} 172 */ 173 @Deprecated 174 public void selectLastImage() { 175 setSelectedImageIndex(data.size() - 1); 176 } 177 178 /** 179 * Check if there is a next image in the sequence 180 * @return {@code true} is there is a next image, {@code false} otherwise 181 */ 182 public boolean hasNextImage() { 183 return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) != data.size() - 1); 184 } 185 186 /** 187 * Search for images in a bounds 188 * @param bounds The bounds to search 189 * @return images in the bounds 190 * @since 17459 191 */ 192 public Collection<ImageEntry> searchImages(Bounds bounds) { 193 return this.geoImages.search(bounds.toBBox()); 194 } 195 196 /** 197 * Get the image next to the current image 198 * @return The next image 199 * @since 18246 200 */ 201 public ImageEntry getNextImage() { 202 if (this.hasNextImage()) { 203 return this.data.get(this.selectedImagesIndex.get(0) + 1); 204 } 205 return null; 206 } 207 208 /** 209 * Select the next image of the sequence 210 * @deprecated Use {@link #getNextImage()} in conjunction with {@link #setSelectedImage} 211 */ 212 @Deprecated 213 public void selectNextImage() { 214 if (hasNextImage()) { 215 setSelectedImageIndex(selectedImagesIndex.get(0) + 1); 216 } 217 } 218 219 /** 220 * Get the image previous to the current image 221 * @return The previous image 222 * @since 18246 223 */ 224 public ImageEntry getPreviousImage() { 225 if (this.hasPreviousImage()) { 226 return this.data.get(Integer.max(0, selectedImagesIndex.get(0) - 1)); 227 } 228 return null; 229 } 230 231 /** 232 * Check if there is a previous image in the sequence 233 * @return {@code true} is there is a previous image, {@code false} otherwise 234 */ 235 public boolean hasPreviousImage() { 236 return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) - 1 > -1); 237 } 238 239 /** 240 * Select the previous image of the sequence 241 * @deprecated Use {@link #getPreviousImage()} with {@link #setSelectedImage} 242 */ 243 @Deprecated 244 public void selectPreviousImage() { 245 if (data.isEmpty()) { 246 return; 247 } 248 setSelectedImageIndex(Integer.max(0, selectedImagesIndex.get(0) - 1)); 249 } 250 251 /** 252 * Select as the selected the given image 253 * @param image the selected image 254 */ 255 public void setSelectedImage(ImageEntry image) { 256 setSelectedImageIndex(data.indexOf(image)); 257 } 258 259 /** 260 * Add image to the list of selected images 261 * @param image {@link ImageEntry} the image to add 262 * @since 15333 263 */ 264 public void addImageToSelection(ImageEntry image) { 265 int index = data.indexOf(image); 266 if (selectedImagesIndex.get(0) == -1) { 267 setSelectedImage(image); 268 } else if (!selectedImagesIndex.contains(index)) { 269 selectedImagesIndex.add(index); 270 listeners.fireEvent(l -> l.selectedImageChanged(this)); 271 } 272 } 273 274 /** 275 * Indicate that an entry has changed 276 * @param gpxImageEntry The entry to update 277 * @since 17574 278 */ 279 public void fireNodeMoved(ImageEntry gpxImageEntry) { 280 this.geoImages.remove(gpxImageEntry); 281 this.geoImages.add(gpxImageEntry); 282 } 283 284 /** 285 * Remove the image from the list of selected images 286 * @param image {@link ImageEntry} the image to remove 287 * @since 15333 288 */ 289 public void removeImageToSelection(ImageEntry image) { 290 int index = data.indexOf(image); 291 selectedImagesIndex.remove(selectedImagesIndex.indexOf(index)); 292 if (selectedImagesIndex.isEmpty()) { 293 selectedImagesIndex.add(-1); 294 } 295 listeners.fireEvent(l -> l.selectedImageChanged(this)); 296 } 297 298 /** 299 * Clear the selected image(s) 300 */ 301 public void clearSelectedImage() { 302 setSelectedImageIndex(-1); 303 } 304 305 private void setSelectedImageIndex(int index) { 306 setSelectedImageIndex(index, false); 307 } 308 309 private void setSelectedImageIndex(int index, boolean forceTrigger) { 310 if (selectedImagesIndex.size() > 1) { 311 selectedImagesIndex.clear(); 312 selectedImagesIndex.add(-1); 313 } 314 if (index == selectedImagesIndex.get(0) && !forceTrigger) { 315 return; 316 } 317 selectedImagesIndex.set(0, index); 318 listeners.fireEvent(l -> l.selectedImageChanged(this)); 319 } 320 321 /** 322 * Remove the current selected image from the list 323 * @since 15348 324 */ 325 public void removeSelectedImages() { 326 removeImages(getSelectedImages()); 327 } 328 329 private void removeImages(List<ImageEntry> selectedImages) { 330 if (selectedImages.isEmpty()) { 331 return; 332 } 333 for (ImageEntry img: getSelectedImages()) { 334 removeImage(img, false); 335 } 336 updateSelectedImage(); 337 } 338 339 /** 340 * Update the selected image after removal of one or more images. 341 * @since 18049 342 */ 343 public void updateSelectedImage() { 344 int size = data.size(); 345 Integer firstSelectedImageIndex = selectedImagesIndex.get(0); 346 if (firstSelectedImageIndex >= size) { 347 setSelectedImageIndex(size - 1); 348 } else { 349 setSelectedImageIndex(firstSelectedImageIndex, true); 350 } 351 } 352 353 /** 354 * Determines if the image is selected 355 * @param image the {@link ImageEntry} image 356 * @return {@code true} is the image is selected, {@code false} otherwise 357 * @since 15333 358 */ 359 public boolean isImageSelected(ImageEntry image) { 360 return selectedImagesIndex.contains(data.indexOf(image)); 361 } 362 363 /** 364 * Remove the image from the list and trigger update listener 365 * @param img the {@link ImageEntry} to remove 366 */ 367 public void removeImage(ImageEntry img) { 368 removeImage(img, true); 369 } 370 371 /** 372 * Remove the image from the list and optionnally trigger update listener 373 * @param img the {@link ImageEntry} to remove 374 * @param fireUpdateEvent if {@code true}, notifies listeners of image update 375 * @since 18049 376 */ 377 public void removeImage(ImageEntry img, boolean fireUpdateEvent) { 378 data.remove(img); 379 this.geoImages.remove(img); 380 if (fireUpdateEvent) { 381 notifyImageUpdate(); 382 } 383 } 384 385 /** 386 * Update the position of the image and trigger update 387 * @param img the image to update 388 * @param newPos the new position 389 */ 390 public void updateImagePosition(ImageEntry img, LatLon newPos) { 391 img.setPos(newPos); 392 this.geoImages.remove(img); 393 this.geoImages.add(img); 394 afterImageUpdated(img); 395 } 396 397 /** 398 * Update the image direction of the image and trigger update 399 * @param img the image to update 400 * @param direction the new direction 401 */ 402 public void updateImageDirection(ImageEntry img, double direction) { 403 img.setExifImgDir(direction); 404 afterImageUpdated(img); 405 } 406 407 /** 408 * Manually trigger the {@link ImageDataUpdateListener#imageDataUpdated(ImageData)} 409 */ 410 public void notifyImageUpdate() { 411 listeners.fireEvent(l -> l.imageDataUpdated(this)); 412 } 413 414 private void afterImageUpdated(ImageEntry img) { 415 img.flagNewGpsData(); 416 notifyImageUpdate(); 417 } 418 419 /** 420 * Add a listener that listens to image data changes 421 * @param listener the {@link ImageDataUpdateListener} 422 */ 423 public void addImageDataUpdateListener(ImageDataUpdateListener listener) { 424 listeners.addListener(listener); 425 } 426 427 /** 428 * Removes a listener that listens to image data changes 429 * @param listener The listener 430 */ 431 public void removeImageDataUpdateListener(ImageDataUpdateListener listener) { 432 listeners.removeListener(listener); 433 } 434 435 @Override 436 public Collection<DataSource> getDataSources() { 437 return Collections.emptyList(); 438 } 439}