001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.Collections; 008import java.util.List; 009import java.util.stream.Collectors; 010 011import org.openstreetmap.josm.data.coor.LatLon; 012import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry; 013import org.openstreetmap.josm.tools.ListenerList; 014 015/** 016 * Class to hold {@link ImageEntry} and the current selection 017 * @since 14590 018 */ 019public class ImageData { 020 /** 021 * A listener that is informed when the current selection change 022 */ 023 public interface ImageDataUpdateListener { 024 /** 025 * Called when the data change 026 * @param data the image data 027 */ 028 void imageDataUpdated(ImageData data); 029 030 /** 031 * Called when the selection change 032 * @param data the image data 033 */ 034 void selectedImageChanged(ImageData data); 035 } 036 037 private final List<ImageEntry> data; 038 039 private final List<Integer> selectedImagesIndex = new ArrayList<>(); 040 041 private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create(); 042 043 /** 044 * Construct a new image container without images 045 */ 046 public ImageData() { 047 this(null); 048 } 049 050 /** 051 * Construct a new image container with a list of images 052 * @param data the list of {@link ImageEntry} 053 */ 054 public ImageData(List<ImageEntry> data) { 055 if (data != null) { 056 Collections.sort(data); 057 this.data = data; 058 } else { 059 this.data = new ArrayList<>(); 060 } 061 selectedImagesIndex.add(-1); 062 } 063 064 /** 065 * Returns the images 066 * @return the images 067 */ 068 public List<ImageEntry> getImages() { 069 return data; 070 } 071 072 /** 073 * Determines if one image has modified GPS data. 074 * @return {@code true} if data has been modified; {@code false}, otherwise 075 */ 076 public boolean isModified() { 077 for (ImageEntry e : data) { 078 if (e.hasNewGpsData()) { 079 return true; 080 } 081 } 082 return false; 083 } 084 085 /** 086 * Merge 2 ImageData 087 * @param otherData {@link ImageData} to merge 088 */ 089 public void mergeFrom(ImageData otherData) { 090 data.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).map(data::get).collect(Collectors.toList()); 132 } 133 134 /** 135 * Select the first image of the sequence 136 */ 137 public void selectFirstImage() { 138 if (!data.isEmpty()) { 139 setSelectedImageIndex(0); 140 } 141 } 142 143 /** 144 * Select the last image of the sequence 145 */ 146 public void selectLastImage() { 147 setSelectedImageIndex(data.size() - 1); 148 } 149 150 /** 151 * Check if there is a next image in the sequence 152 * @return {@code true} is there is a next image, {@code false} otherwise 153 */ 154 public boolean hasNextImage() { 155 return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) != data.size() - 1); 156 } 157 158 /** 159 * Select the next image of the sequence 160 */ 161 public void selectNextImage() { 162 if (hasNextImage()) { 163 setSelectedImageIndex(selectedImagesIndex.get(0) + 1); 164 } 165 } 166 167 /** 168 * Check if there is a previous image in the sequence 169 * @return {@code true} is there is a previous image, {@code false} otherwise 170 */ 171 public boolean hasPreviousImage() { 172 return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) - 1 > -1); 173 } 174 175 /** 176 * Select the previous image of the sequence 177 */ 178 public void selectPreviousImage() { 179 if (data.isEmpty()) { 180 return; 181 } 182 setSelectedImageIndex(Integer.max(0, selectedImagesIndex.get(0) - 1)); 183 } 184 185 /** 186 * Select as the selected the given image 187 * @param image the selected image 188 */ 189 public void setSelectedImage(ImageEntry image) { 190 setSelectedImageIndex(data.indexOf(image)); 191 } 192 193 /** 194 * Add image to the list of selected images 195 * @param image {@link ImageEntry} the image to add 196 * @since 15333 197 */ 198 public void addImageToSelection(ImageEntry image) { 199 int index = data.indexOf(image); 200 if (selectedImagesIndex.get(0) == -1) { 201 setSelectedImage(image); 202 } else if (!selectedImagesIndex.contains(index)) { 203 selectedImagesIndex.add(index); 204 listeners.fireEvent(l -> l.selectedImageChanged(this)); 205 } 206 } 207 208 /** 209 * Remove the image from the list of selected images 210 * @param image {@link ImageEntry} the image to remove 211 * @since 15333 212 */ 213 public void removeImageToSelection(ImageEntry image) { 214 int index = data.indexOf(image); 215 selectedImagesIndex.remove(selectedImagesIndex.indexOf(index)); 216 if (selectedImagesIndex.isEmpty()) { 217 selectedImagesIndex.add(-1); 218 } 219 listeners.fireEvent(l -> l.selectedImageChanged(this)); 220 } 221 222 /** 223 * Clear the selected image(s) 224 */ 225 public void clearSelectedImage() { 226 setSelectedImageIndex(-1); 227 } 228 229 private void setSelectedImageIndex(int index) { 230 setSelectedImageIndex(index, false); 231 } 232 233 private void setSelectedImageIndex(int index, boolean forceTrigger) { 234 if (selectedImagesIndex.size() > 1) { 235 selectedImagesIndex.clear(); 236 selectedImagesIndex.add(-1); 237 } 238 if (index == selectedImagesIndex.get(0) && !forceTrigger) { 239 return; 240 } 241 selectedImagesIndex.set(0, index); 242 listeners.fireEvent(l -> l.selectedImageChanged(this)); 243 } 244 245 /** 246 * Remove the current selected image from the list 247 */ 248 public void removeSelectedImage() { 249 List<ImageEntry> selectedImages = getSelectedImages(); 250 if (selectedImages.size() > 1) { 251 throw new IllegalStateException(tr("Multiple images have been selected")); 252 } 253 removeImages(selectedImages); 254 } 255 256 /** 257 * Remove the current selected image from the list 258 * @since 15348 259 */ 260 public void removeSelectedImages() { 261 List<ImageEntry> selectedImages = getSelectedImages(); 262 removeImages(selectedImages); 263 } 264 265 private void removeImages(List<ImageEntry> selectedImages) { 266 if (selectedImages.isEmpty()) { 267 return; 268 } 269 for (ImageEntry img: getSelectedImages()) { 270 data.remove(img); 271 } 272 if (selectedImagesIndex.get(0) == data.size()) { 273 setSelectedImageIndex(data.size() - 1); 274 } else { 275 setSelectedImageIndex(selectedImagesIndex.get(0), true); 276 } 277 } 278 279 /** 280 * Determines if the image is selected 281 * @param image the {@link ImageEntry} image 282 * @return {@code true} is the image is selected, {@code false} otherwise 283 * @since 15333 284 */ 285 public boolean isImageSelected(ImageEntry image) { 286 int index = data.indexOf(image); 287 return selectedImagesIndex.contains(index); 288 } 289 290 /** 291 * Remove the image from the list and trigger update listener 292 * @param img the {@link ImageEntry} to remove 293 */ 294 public void removeImage(ImageEntry img) { 295 data.remove(img); 296 notifyImageUpdate(); 297 } 298 299 /** 300 * Update the position of the image and trigger update 301 * @param img the image to update 302 * @param newPos the new position 303 */ 304 public void updateImagePosition(ImageEntry img, LatLon newPos) { 305 img.setPos(newPos); 306 afterImageUpdated(img); 307 } 308 309 /** 310 * Update the image direction of the image and trigger update 311 * @param img the image to update 312 * @param direction the new direction 313 */ 314 public void updateImageDirection(ImageEntry img, double direction) { 315 img.setExifImgDir(direction); 316 afterImageUpdated(img); 317 } 318 319 /** 320 * Manually trigger the {@link ImageDataUpdateListener#imageDataUpdated(ImageData)} 321 */ 322 public void notifyImageUpdate() { 323 listeners.fireEvent(l -> l.imageDataUpdated(this)); 324 } 325 326 private void afterImageUpdated(ImageEntry img) { 327 img.flagNewGpsData(); 328 notifyImageUpdate(); 329 } 330 331 /** 332 * Add a listener that listens to image data changes 333 * @param listener the {@link ImageDataUpdateListener} 334 */ 335 public void addImageDataUpdateListener(ImageDataUpdateListener listener) { 336 listeners.addListener(listener); 337 } 338 339 /** 340 * Removes a listener that listens to image data changes 341 * @param listener The listener 342 */ 343 public void removeImageDataUpdateListener(ImageDataUpdateListener listener) { 344 listeners.removeListener(listener); 345 } 346}