source: josm/trunk/src/org/openstreetmap/josm/data/ImageData.java

Last change on this file was 18591, checked in by taylor.smock, 18 months ago

Fix #21605: Add tabs to ImageViewerDialog for use with different image layers

This allows users to have multiple geotagged image layers, and
quickly switch between them.

  • Property svn:eol-style set to native
File size: 12.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import java.util.ArrayList;
5import java.util.Collection;
6import java.util.Collections;
7import java.util.List;
8import java.util.stream.Collectors;
9
10import org.openstreetmap.josm.data.coor.LatLon;
11import org.openstreetmap.josm.data.gpx.GpxImageEntry;
12import org.openstreetmap.josm.data.osm.QuadBuckets;
13import org.openstreetmap.josm.gui.layer.Layer;
14import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
15import org.openstreetmap.josm.tools.ListenerList;
16
17/**
18 * Class to hold {@link ImageEntry} and the current selection
19 * @since 14590
20 */
21public class ImageData implements Data {
22 /**
23 * A listener that is informed when the current selection change
24 */
25 public interface ImageDataUpdateListener {
26 /**
27 * Called when the data change
28 * @param data the image data
29 */
30 void imageDataUpdated(ImageData data);
31
32 /**
33 * Called when the selection change
34 * @param data the image data
35 */
36 void selectedImageChanged(ImageData data);
37 }
38
39 private final List<ImageEntry> data;
40
41 private final List<Integer> selectedImagesIndex = new ArrayList<>();
42
43 private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create();
44 private final QuadBuckets<ImageEntry> geoImages = new QuadBuckets<>();
45 private Layer layer;
46
47 /**
48 * Construct a new image container without images
49 */
50 public ImageData() {
51 this(null);
52 }
53
54 /**
55 * Construct a new image container with a list of images
56 * @param data the list of {@link ImageEntry}
57 */
58 public ImageData(List<ImageEntry> data) {
59 if (data != null) {
60 Collections.sort(data);
61 this.data = data;
62 this.data.forEach(image -> image.setDataSet(this));
63 } else {
64 this.data = new ArrayList<>();
65 }
66 this.geoImages.addAll(this.data);
67 selectedImagesIndex.add(-1);
68 }
69
70 /**
71 * Returns the images
72 * @return the images
73 */
74 public List<ImageEntry> getImages() {
75 return data;
76 }
77
78 /**
79 * Determines if one image has modified GPS data.
80 * @return {@code true} if data has been modified; {@code false}, otherwise
81 */
82 public boolean isModified() {
83 return data.stream().anyMatch(GpxImageEntry::hasNewGpsData);
84 }
85
86 /**
87 * Merge 2 ImageData
88 * @param otherData {@link ImageData} to merge
89 */
90 public void mergeFrom(ImageData otherData) {
91 data.addAll(otherData.getImages());
92 this.geoImages.addAll(otherData.getImages());
93 Collections.sort(data);
94
95 final ImageEntry selected = otherData.getSelectedImage();
96
97 // Suppress the double photos.
98 if (data.size() > 1) {
99 ImageEntry prev = data.get(data.size() - 1);
100 for (int i = data.size() - 2; i >= 0; i--) {
101 ImageEntry cur = data.get(i);
102 if (cur.getFile().equals(prev.getFile())) {
103 data.remove(i);
104 } else {
105 prev = cur;
106 }
107 }
108 }
109 if (selected != null) {
110 setSelectedImageIndex(data.indexOf(selected));
111 }
112 }
113
114 /**
115 * Return the first currently selected image
116 * @return the first selected image as {@link ImageEntry} or null
117 * @see #getSelectedImages
118 */
119 public ImageEntry getSelectedImage() {
120 int selectedImageIndex = selectedImagesIndex.isEmpty() ? -1 : selectedImagesIndex.get(0);
121 if (selectedImageIndex > -1) {
122 return data.get(selectedImageIndex);
123 }
124 return null;
125 }
126
127 /**
128 * Return the current selected images
129 * @return the selected images as list {@link ImageEntry}
130 * @since 15333
131 */
132 public List<ImageEntry> getSelectedImages() {
133 return selectedImagesIndex.stream().filter(i -> i > -1 && i < data.size()).map(data::get).collect(Collectors.toList());
134 }
135
136 /**
137 * Get the first image on the layer
138 * @return The first image
139 * @since 18246
140 */
141 public ImageEntry getFirstImage() {
142 if (!this.data.isEmpty()) {
143 return this.data.get(0);
144 }
145 return null;
146 }
147
148 /**
149 * Get the last image in the layer
150 * @return The last image
151 * @since 18246
152 */
153 public ImageEntry getLastImage() {
154 if (!this.data.isEmpty()) {
155 return this.data.get(this.data.size() - 1);
156 }
157 return null;
158 }
159
160 /**
161 * Check if there is a next image in the sequence
162 * @return {@code true} is there is a next image, {@code false} otherwise
163 */
164 public boolean hasNextImage() {
165 return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) != data.size() - 1);
166 }
167
168 /**
169 * Search for images in a bounds
170 * @param bounds The bounds to search
171 * @return images in the bounds
172 * @since 17459
173 */
174 public Collection<ImageEntry> searchImages(Bounds bounds) {
175 return this.geoImages.search(bounds.toBBox());
176 }
177
178 /**
179 * Get the image next to the current image
180 * @return The next image
181 * @since 18246
182 */
183 public ImageEntry getNextImage() {
184 if (this.hasNextImage()) {
185 return this.data.get(this.selectedImagesIndex.get(0) + 1);
186 }
187 return null;
188 }
189
190 /**
191 * Get the image previous to the current image
192 * @return The previous image
193 * @since 18246
194 */
195 public ImageEntry getPreviousImage() {
196 if (this.hasPreviousImage()) {
197 return this.data.get(Integer.max(0, selectedImagesIndex.get(0) - 1));
198 }
199 return null;
200 }
201
202 /**
203 * Check if there is a previous image in the sequence
204 * @return {@code true} is there is a previous image, {@code false} otherwise
205 */
206 public boolean hasPreviousImage() {
207 return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) - 1 > -1);
208 }
209
210 /**
211 * Select as the selected the given image
212 * @param image the selected image
213 */
214 public void setSelectedImage(ImageEntry image) {
215 setSelectedImageIndex(data.indexOf(image));
216 }
217
218 /**
219 * Add image to the list of selected images
220 * @param image {@link ImageEntry} the image to add
221 * @since 15333
222 */
223 public void addImageToSelection(ImageEntry image) {
224 int index = data.indexOf(image);
225 if (selectedImagesIndex.get(0) == -1) {
226 setSelectedImage(image);
227 } else if (!selectedImagesIndex.contains(index)) {
228 selectedImagesIndex.add(index);
229 listeners.fireEvent(l -> l.selectedImageChanged(this));
230 }
231 }
232
233 /**
234 * Indicate that an entry has changed
235 * @param gpxImageEntry The entry to update
236 * @since 17574
237 */
238 public void fireNodeMoved(ImageEntry gpxImageEntry) {
239 this.geoImages.remove(gpxImageEntry);
240 this.geoImages.add(gpxImageEntry);
241 }
242
243 /**
244 * Remove the image from the list of selected images
245 * @param image {@link ImageEntry} the image to remove
246 * @since 15333
247 */
248 public void removeImageToSelection(ImageEntry image) {
249 int index = data.indexOf(image);
250 selectedImagesIndex.remove(selectedImagesIndex.indexOf(index));
251 if (selectedImagesIndex.isEmpty()) {
252 selectedImagesIndex.add(-1);
253 }
254 listeners.fireEvent(l -> l.selectedImageChanged(this));
255 }
256
257 /**
258 * Clear the selected image(s)
259 */
260 public void clearSelectedImage() {
261 setSelectedImageIndex(-1);
262 }
263
264 private void setSelectedImageIndex(int index) {
265 setSelectedImageIndex(index, false);
266 }
267
268 private void setSelectedImageIndex(int index, boolean forceTrigger) {
269 if (selectedImagesIndex.size() > 1) {
270 selectedImagesIndex.clear();
271 selectedImagesIndex.add(-1);
272 }
273 if (index == selectedImagesIndex.get(0) && !forceTrigger) {
274 return;
275 }
276 selectedImagesIndex.set(0, index);
277 listeners.fireEvent(l -> l.selectedImageChanged(this));
278 }
279
280 /**
281 * Remove the current selected image from the list
282 * @since 15348
283 */
284 public void removeSelectedImages() {
285 removeImages(getSelectedImages());
286 }
287
288 private void removeImages(List<ImageEntry> selectedImages) {
289 if (selectedImages.isEmpty()) {
290 return;
291 }
292 for (ImageEntry img: getSelectedImages()) {
293 removeImage(img, false);
294 }
295 updateSelectedImage();
296 }
297
298 /**
299 * Update the selected image after removal of one or more images.
300 * @since 18049
301 */
302 public void updateSelectedImage() {
303 int size = data.size();
304 Integer firstSelectedImageIndex = selectedImagesIndex.get(0);
305 if (firstSelectedImageIndex >= size) {
306 setSelectedImageIndex(size - 1);
307 } else {
308 setSelectedImageIndex(firstSelectedImageIndex, true);
309 }
310 }
311
312 /**
313 * Determines if the image is selected
314 * @param image the {@link ImageEntry} image
315 * @return {@code true} is the image is selected, {@code false} otherwise
316 * @since 15333
317 */
318 public boolean isImageSelected(ImageEntry image) {
319 return selectedImagesIndex.contains(data.indexOf(image));
320 }
321
322 /**
323 * Remove the image from the list and trigger update listener
324 * @param img the {@link ImageEntry} to remove
325 */
326 public void removeImage(ImageEntry img) {
327 removeImage(img, true);
328 }
329
330 /**
331 * Remove the image from the list and optionally trigger update listener
332 * @param img the {@link ImageEntry} to remove
333 * @param fireUpdateEvent if {@code true}, notifies listeners of image update
334 * @since 18049
335 */
336 public void removeImage(ImageEntry img, boolean fireUpdateEvent) {
337 data.remove(img);
338 this.geoImages.remove(img);
339 if (fireUpdateEvent) {
340 notifyImageUpdate();
341 // Fix JOSM #21521 -- when an image is removed, we need to update the selected image
342 this.updateSelectedImage();
343 }
344 }
345
346 /**
347 * Update the position of the image and trigger update
348 * @param img the image to update
349 * @param newPos the new position
350 */
351 public void updateImagePosition(ImageEntry img, LatLon newPos) {
352 img.setPos(newPos);
353 this.geoImages.remove(img);
354 this.geoImages.add(img);
355 afterImageUpdated(img);
356 }
357
358 /**
359 * Update the image direction of the image and trigger update
360 * @param img the image to update
361 * @param direction the new direction
362 */
363 public void updateImageDirection(ImageEntry img, double direction) {
364 img.setExifImgDir(direction);
365 afterImageUpdated(img);
366 }
367
368 /**
369 * Manually trigger the {@link ImageDataUpdateListener#imageDataUpdated(ImageData)}
370 */
371 public void notifyImageUpdate() {
372 listeners.fireEvent(l -> l.imageDataUpdated(this));
373 }
374
375 private void afterImageUpdated(ImageEntry img) {
376 img.flagNewGpsData();
377 notifyImageUpdate();
378 }
379
380 /**
381 * Set the layer for use with {@link org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog#displayImages(Layer, List)}
382 * @param layer The layer to use for organization
383 * @since 18591
384 */
385 public void setLayer(Layer layer) {
386 this.layer = layer;
387 }
388
389 /**
390 * Get the layer that this data is associated with. May be {@code null}.
391 * @return The layer this data is associated with.
392 * @since 18591
393 */
394 public Layer getLayer() {
395 return this.layer;
396 }
397
398 /**
399 * Add a listener that listens to image data changes
400 * @param listener the {@link ImageDataUpdateListener}
401 */
402 public void addImageDataUpdateListener(ImageDataUpdateListener listener) {
403 listeners.addListener(listener);
404 }
405
406 /**
407 * Removes a listener that listens to image data changes
408 * @param listener The listener
409 */
410 public void removeImageDataUpdateListener(ImageDataUpdateListener listener) {
411 listeners.removeListener(listener);
412 }
413
414 @Override
415 public Collection<DataSource> getDataSources() {
416 return Collections.emptyList();
417 }
418}
Note: See TracBrowser for help on using the repository browser.