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}