001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.Comparator;
008import java.util.Date;
009import java.util.List;
010import java.util.Map;
011
012import org.openstreetmap.josm.data.UserIdentityManager;
013import org.openstreetmap.josm.data.coor.LatLon;
014import org.openstreetmap.josm.data.notes.Note;
015import org.openstreetmap.josm.data.notes.Note.State;
016import org.openstreetmap.josm.data.notes.NoteComment;
017import org.openstreetmap.josm.tools.ListenerList;
018import org.openstreetmap.josm.tools.Logging;
019
020/**
021 * Class to hold and perform operations on a set of notes
022 */
023public class NoteData {
024
025    /**
026     * A listener that can be informed on note data changes.
027     * @author Michael Zangl
028     * @since 12343
029     */
030    public interface NoteDataUpdateListener {
031        /**
032         * Called when the note data is updated
033         * @param data The data that was changed
034         */
035        void noteDataUpdated(NoteData data);
036
037        /**
038         * The selected node was changed
039         * @param noteData The data of which the selected node was changed
040         */
041        void selectedNoteChanged(NoteData noteData);
042    }
043
044    private long newNoteId = -1;
045
046    private final Storage<Note> noteList;
047    private Note selectedNote;
048    private Comparator<Note> comparator = Note.DEFAULT_COMPARATOR;
049
050    private final ListenerList<NoteDataUpdateListener> listeners = ListenerList.create();
051
052    /**
053     * Construct a new note container without notes
054     * @since 14101
055     */
056    public NoteData() {
057        this(null);
058    }
059
060    /**
061     * Construct a new note container with a given list of notes
062     * @param notes The list of notes to populate the container with
063     */
064    public NoteData(Collection<Note> notes) {
065        noteList = new Storage<>();
066        if (notes != null) {
067            for (Note note : notes) {
068                noteList.add(note);
069                if (note.getId() <= newNoteId) {
070                    newNoteId = note.getId() - 1;
071                }
072            }
073        }
074    }
075
076    /**
077     * Returns the notes stored in this layer
078     * @return collection of notes
079     */
080    public Collection<Note> getNotes() {
081        return Collections.unmodifiableCollection(noteList);
082    }
083
084    /**
085     * Returns the notes stored in this layer sorted according to {@link #comparator}
086     * @return sorted collection of notes
087     */
088    public Collection<Note> getSortedNotes() {
089        final List<Note> list = new ArrayList<>(noteList);
090        list.sort(comparator);
091        return list;
092    }
093
094    /**
095     * Returns the currently selected note
096     * @return currently selected note
097     */
098    public Note getSelectedNote() {
099        return selectedNote;
100    }
101
102    /**
103     * Set a selected note. Causes the dialog to select the note and
104     * the note layer to draw the selected note's comments.
105     * @param note Selected note. Null indicates no selection
106     */
107    public void setSelectedNote(Note note) {
108        selectedNote = note;
109        listeners.fireEvent(l -> l.selectedNoteChanged(this));
110    }
111
112    /**
113     * Return whether or not there are any changes in the note data set.
114     * These changes may need to be either uploaded or saved.
115     * @return true if local modifications have been made to the note data set. False otherwise.
116     */
117    public synchronized boolean isModified() {
118        for (Note note : noteList) {
119            if (note.getId() < 0) { //notes with negative IDs are new
120                return true;
121            }
122            for (NoteComment comment : note.getComments()) {
123                if (comment.isNew()) {
124                    return true;
125                }
126            }
127        }
128        return false;
129    }
130
131    /**
132     * Merge notes from an existing note data.
133     * @param from existing note data
134     * @since 13437
135     */
136    public synchronized void mergeFrom(NoteData from) {
137        if (this != from) {
138            addNotes(from.noteList);
139        }
140    }
141
142    /**
143     * Add notes to the data set. It only adds a note if the ID is not already present
144     * @param newNotes A list of notes to add
145     */
146    public synchronized void addNotes(Collection<Note> newNotes) {
147        for (Note newNote : newNotes) {
148            if (!noteList.contains(newNote)) {
149                noteList.add(newNote);
150            } else {
151                final Note existingNote = noteList.get(newNote);
152                final boolean isDirty = existingNote.getComments().stream().anyMatch(NoteComment::isNew);
153                if (!isDirty) {
154                    noteList.put(newNote);
155                } else {
156                    // TODO merge comments?
157                    Logging.info("Keeping existing note id={0} with uncommitted changes", String.valueOf(newNote.getId()));
158                }
159            }
160            if (newNote.getId() <= newNoteId) {
161                newNoteId = newNote.getId() - 1;
162            }
163        }
164        dataUpdated();
165    }
166
167    /**
168     * Create a new note
169     * @param location Location of note
170     * @param text Required comment with which to open the note
171     */
172    public synchronized void createNote(LatLon location, String text) {
173        if (text == null || text.isEmpty()) {
174            throw new IllegalArgumentException("Comment can not be blank when creating a note");
175        }
176        Note note = new Note(location);
177        note.setCreatedAt(new Date());
178        note.setState(State.OPEN);
179        note.setId(newNoteId--);
180        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.OPENED, true);
181        note.addComment(comment);
182        if (Logging.isDebugEnabled()) {
183            Logging.debug("Created note {0} with comment: {1}", note.getId(), text);
184        }
185        noteList.add(note);
186        dataUpdated();
187    }
188
189    /**
190     * Add a new comment to an existing note
191     * @param note Note to add comment to. Must already exist in the layer
192     * @param text Comment to add
193     */
194    public synchronized void addCommentToNote(Note note, String text) {
195        if (!noteList.contains(note)) {
196            throw new IllegalArgumentException("Note to modify must be in layer");
197        }
198        if (note.getState() == State.CLOSED) {
199            throw new IllegalStateException("Cannot add a comment to a closed note");
200        }
201        if (Logging.isDebugEnabled()) {
202            Logging.debug("Adding comment to note {0}: {1}", note.getId(), text);
203        }
204        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.COMMENTED, true);
205        note.addComment(comment);
206        dataUpdated();
207    }
208
209    /**
210     * Close note with comment
211     * @param note Note to close. Must already exist in the layer
212     * @param text Comment to attach to close action, if desired
213     */
214    public synchronized void closeNote(Note note, String text) {
215        if (!noteList.contains(note)) {
216            throw new IllegalArgumentException("Note to close must be in layer");
217        }
218        if (note.getState() != State.OPEN) {
219            throw new IllegalStateException("Cannot close a note that isn't open");
220        }
221        if (Logging.isDebugEnabled()) {
222            Logging.debug("closing note {0} with comment: {1}", note.getId(), text);
223        }
224        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.CLOSED, true);
225        note.addComment(comment);
226        note.setState(State.CLOSED);
227        note.setClosedAt(new Date());
228        dataUpdated();
229    }
230
231    /**
232     * Reopen a closed note.
233     * @param note Note to reopen. Must already exist in the layer
234     * @param text Comment to attach to the reopen action, if desired
235     */
236    public synchronized void reOpenNote(Note note, String text) {
237        if (!noteList.contains(note)) {
238            throw new IllegalArgumentException("Note to reopen must be in layer");
239        }
240        if (note.getState() != State.CLOSED) {
241            throw new IllegalStateException("Cannot reopen a note that isn't closed");
242        }
243        Logging.debug("reopening note {0} with comment: {1}", note.getId(), text);
244        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.REOPENED, true);
245        note.addComment(comment);
246        note.setState(State.OPEN);
247        dataUpdated();
248    }
249
250    private void dataUpdated() {
251        listeners.fireEvent(l -> l.noteDataUpdated(this));
252    }
253
254    private static User getCurrentUser() {
255        UserIdentityManager userMgr = UserIdentityManager.getInstance();
256        return User.createOsmUser(userMgr.getUserId(), userMgr.getUserName());
257    }
258
259    /**
260     * Updates notes with new state. Primarily to be used when updating the
261     * note layer after uploading note changes to the server.
262     * @param updatedNotes Map containing the original note as the key and the updated note as the value
263     */
264    public synchronized void updateNotes(Map<Note, Note> updatedNotes) {
265        for (Map.Entry<Note, Note> entry : updatedNotes.entrySet()) {
266            Note oldNote = entry.getKey();
267            Note newNote = entry.getValue();
268            boolean reindex = oldNote.hashCode() != newNote.hashCode();
269            if (reindex) {
270                noteList.removeElem(oldNote);
271            }
272            oldNote.updateWith(newNote);
273            if (reindex) {
274                noteList.add(oldNote);
275            }
276        }
277        dataUpdated();
278    }
279
280    /**
281     * Returns the current comparator being used to sort the note list.
282     * @return The current comparator being used to sort the note list
283     */
284    public Comparator<Note> getCurrentSortMethod() {
285        return comparator;
286    }
287
288    /** Set the comparator to be used to sort the note list. Several are available
289     * as public static members of this class.
290     * @param comparator - The Note comparator to sort by
291     */
292    public void setSortMethod(Comparator<Note> comparator) {
293        this.comparator = comparator;
294        dataUpdated();
295    }
296
297    /**
298     * Adds a listener that listens to node data changes
299     * @param listener The listener
300     */
301    public void addNoteDataUpdateListener(NoteDataUpdateListener listener) {
302        listeners.addListener(listener);
303    }
304
305    /**
306     * Removes a listener that listens to node data changes
307     * @param listener The listener
308     */
309    public void removeNoteDataUpdateListener(NoteDataUpdateListener listener) {
310        listeners.removeListener(listener);
311    }
312}