001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import java.util.ArrayList; 005import java.util.Collections; 006import java.util.List; 007 008import org.openstreetmap.josm.data.coor.LatLon; 009import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry; 010import org.openstreetmap.josm.tools.ListenerList; 011 012/** 013 * Class to hold {@link ImageEntry} and the current selection 014 * @since 14590 015 */ 016public class ImageData { 017 /** 018 * A listener that is informed when the current selection change 019 */ 020 public interface ImageDataUpdateListener { 021 /** 022 * Called when the data change 023 * @param data the image data 024 */ 025 void imageDataUpdated(ImageData data); 026 027 /** 028 * Called when the selection change 029 * @param data the image data 030 */ 031 void selectedImageChanged(ImageData data); 032 } 033 034 private final List<ImageEntry> data; 035 036 private int selectedImageIndex = -1; 037 038 private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create(); 039 040 /** 041 * Construct a new image container without images 042 */ 043 public ImageData() { 044 this(null); 045 } 046 047 /** 048 * Construct a new image container with a list of images 049 * @param data the list of {@link ImageEntry} 050 */ 051 public ImageData(List<ImageEntry> data) { 052 if (data != null) { 053 Collections.sort(data); 054 this.data = data; 055 } else { 056 this.data = new ArrayList<>(); 057 } 058 } 059 060 /** 061 * Returns the images 062 * @return the images 063 */ 064 public List<ImageEntry> getImages() { 065 return data; 066 } 067 068 /** 069 * Determines if one image has modified GPS data. 070 * @return {@code true} if data has been modified; {@code false}, otherwise 071 */ 072 public boolean isModified() { 073 for (ImageEntry e : data) { 074 if (e.hasNewGpsData()) { 075 return true; 076 } 077 } 078 return false; 079 } 080 081 /** 082 * Merge 2 ImageData 083 * @param otherData {@link ImageData} to merge 084 */ 085 public void mergeFrom(ImageData otherData) { 086 data.addAll(otherData.getImages()); 087 Collections.sort(data); 088 089 final ImageEntry selected = otherData.getSelectedImage(); 090 091 // Suppress the double photos. 092 if (data.size() > 1) { 093 ImageEntry prev = data.get(data.size() - 1); 094 for (int i = data.size() - 2; i >= 0; i--) { 095 ImageEntry cur = data.get(i); 096 if (cur.getFile().equals(prev.getFile())) { 097 data.remove(i); 098 } else { 099 prev = cur; 100 } 101 } 102 } 103 if (selected != null) { 104 setSelectedImageIndex(data.indexOf(selected)); 105 } 106 } 107 108 /** 109 * Return the current selected image 110 * @return the selected image as {@link ImageEntry} or null 111 */ 112 public ImageEntry getSelectedImage() { 113 if (selectedImageIndex > -1) { 114 return data.get(selectedImageIndex); 115 } 116 return null; 117 } 118 119 /** 120 * Select the first image of the sequence 121 */ 122 public void selectFirstImage() { 123 if (!data.isEmpty()) { 124 setSelectedImageIndex(0); 125 } 126 } 127 128 /** 129 * Select the last image of the sequence 130 */ 131 public void selectLastImage() { 132 setSelectedImageIndex(data.size() - 1); 133 } 134 135 /** 136 * Check if there is a next image in the sequence 137 * @return {@code true} is there is a next image, {@code false} otherwise 138 */ 139 public boolean hasNextImage() { 140 return selectedImageIndex != data.size() - 1; 141 } 142 143 /** 144 * Select the next image of the sequence 145 */ 146 public void selectNextImage() { 147 if (hasNextImage()) { 148 setSelectedImageIndex(selectedImageIndex + 1); 149 } 150 } 151 152 /** 153 * Check if there is a previous image in the sequence 154 * @return {@code true} is there is a previous image, {@code false} otherwise 155 */ 156 public boolean hasPreviousImage() { 157 return selectedImageIndex - 1 > -1; 158 } 159 160 /** 161 * Select the previous image of the sequence 162 */ 163 public void selectPreviousImage() { 164 if (data.isEmpty()) { 165 return; 166 } 167 setSelectedImageIndex(Integer.max(0, selectedImageIndex - 1)); 168 } 169 170 /** 171 * Select as the selected the given image 172 * @param image the selected image 173 */ 174 public void setSelectedImage(ImageEntry image) { 175 setSelectedImageIndex(data.indexOf(image)); 176 } 177 178 /** 179 * Clear the selected image 180 */ 181 public void clearSelectedImage() { 182 setSelectedImageIndex(-1); 183 } 184 185 private void setSelectedImageIndex(int index) { 186 setSelectedImageIndex(index, false); 187 } 188 189 private void setSelectedImageIndex(int index, boolean forceTrigger) { 190 if (index == selectedImageIndex && !forceTrigger) { 191 return; 192 } 193 selectedImageIndex = index; 194 listeners.fireEvent(l -> l.selectedImageChanged(this)); 195 } 196 197 /** 198 * Remove the current selected image from the list 199 */ 200 public void removeSelectedImage() { 201 data.remove(getSelectedImage()); 202 if (selectedImageIndex == data.size()) { 203 setSelectedImageIndex(data.size() - 1); 204 } else { 205 setSelectedImageIndex(selectedImageIndex, true); 206 } 207 } 208 209 /** 210 * Remove the image from the list and trigger update listener 211 * @param img the {@link ImageEntry} to remove 212 */ 213 public void removeImage(ImageEntry img) { 214 data.remove(img); 215 notifyImageUpdate(); 216 } 217 218 /** 219 * Update the position of the image and trigger update 220 * @param img the image to update 221 * @param newPos the new position 222 */ 223 public void updateImagePosition(ImageEntry img, LatLon newPos) { 224 img.setPos(newPos); 225 afterImageUpdated(img); 226 } 227 228 /** 229 * Update the image direction of the image and trigger update 230 * @param img the image to update 231 * @param direction the new direction 232 */ 233 public void updateImageDirection(ImageEntry img, double direction) { 234 img.setExifImgDir(direction); 235 afterImageUpdated(img); 236 } 237 238 /** 239 * Manually trigger the {@link ImageDataUpdateListener#imageDataUpdated(ImageData)} 240 */ 241 public void notifyImageUpdate() { 242 listeners.fireEvent(l -> l.imageDataUpdated(this)); 243 } 244 245 private void afterImageUpdated(ImageEntry img) { 246 img.flagNewGpsData(); 247 notifyImageUpdate(); 248 } 249 250 /** 251 * Add a listener that listens to image data changes 252 * @param listener the {@link ImageDataUpdateListener} 253 */ 254 public void addImageDataUpdateListener(ImageDataUpdateListener listener) { 255 listeners.addListener(listener); 256 } 257 258 /** 259 * Removes a listener that listens to image data changes 260 * @param listener The listener 261 */ 262 public void removeImageDataUpdateListener(ImageDataUpdateListener listener) { 263 listeners.removeListener(listener); 264 } 265}