001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.Comparator;
007import java.util.Date;
008import java.util.List;
009import java.util.Map;
010
011import org.openstreetmap.josm.Main;
012import org.openstreetmap.josm.data.coor.LatLon;
013import org.openstreetmap.josm.data.notes.Note;
014import org.openstreetmap.josm.data.notes.Note.State;
015import org.openstreetmap.josm.data.notes.NoteComment;
016import org.openstreetmap.josm.gui.JosmUserIdentityManager;
017
018/**
019 * Class to hold and perform operations on a set of notes
020 */
021public class NoteData {
022
023    private long newNoteId = -1;
024
025    private final List<Note> noteList;
026    private Note selectedNote = null;
027    private Comparator<Note> comparator = DEFAULT_COMPARATOR;
028
029    /**
030     * Sorts notes in the following order:
031     * 1) Open notes
032     * 2) Closed notes
033     * 3) New notes
034     * Within each subgroup it sorts by ID
035     */
036    public static final Comparator<Note> DEFAULT_COMPARATOR = new Comparator<Note>() {
037        @Override
038        public int compare(Note n1, Note n2) {
039            if (n1.getId() < 0 && n2.getId() > 0) {
040                return 1;
041            }
042            if (n1.getId() > 0 && n2.getId() < 0) {
043                return -1;
044            }
045            if (n1.getState() == State.closed && n2.getState() == State.open) {
046                return 1;
047            }
048            if (n1.getState() == State.open && n2.getState() == State.closed) {
049                return -1;
050            }
051            return Long.valueOf(Math.abs(n1.getId())).compareTo(Long.valueOf(Math.abs(n2.getId())));
052        }
053    };
054
055    /** Sorts notes strictly by creation date */
056    public static final Comparator<Note> DATE_COMPARATOR = new Comparator<Note>() {
057        @Override
058        public int compare(Note n1, Note n2) {
059            return n1.getCreatedAt().compareTo(n2.getCreatedAt());
060        }
061    };
062
063    /** Sorts notes by user, then creation date */
064    public static final Comparator<Note> USER_COMPARATOR = new Comparator<Note>() {
065        @Override
066        public int compare(Note n1, Note n2) {
067            String n1User = n1.getFirstComment().getUser().getName();
068            String n2User = n2.getFirstComment().getUser().getName();
069            if (n1User.equals(n2User)) {
070                return n1.getCreatedAt().compareTo(n2.getCreatedAt());
071            }
072            return n1.getFirstComment().getUser().getName().compareTo(n2.getFirstComment().getUser().getName());
073        }
074    };
075
076    /** Sorts notes by the last modified date */
077    public static final Comparator<Note> LAST_ACTION_COMPARATOR = new Comparator<Note>() {
078        @Override
079        public int compare(Note n1, Note n2) {
080            Date n1Date = n1.getComments().get(n1.getComments().size()-1).getCommentTimestamp();
081            Date n2Date = n2.getComments().get(n2.getComments().size()-1).getCommentTimestamp();
082            return n1Date.compareTo(n2Date);
083        }
084    };
085
086    /**
087     * Construct a new note container with an empty note list
088     */
089    public NoteData() {
090        noteList = new ArrayList<>();
091    }
092
093    /**
094     * Construct a new note container with a given list of notes
095     * @param notes The list of notes to populate the container with
096     */
097    public NoteData(List<Note> notes) {
098        noteList = notes;
099        Collections.sort(notes, comparator);
100        for (Note note : notes) {
101            if (note.getId() <= newNoteId) {
102                newNoteId = note.getId() - 1;
103            }
104        }
105    }
106
107    /**
108     * Returns the notes stored in this layer
109     * @return List of Note objects
110     */
111    public List<Note> getNotes() {
112        return noteList;
113    }
114
115    /** Returns the currently selected note
116     * @return currently selected note
117     */
118    public Note getSelectedNote() {
119        return selectedNote;
120    }
121
122    /** Set a selected note. Causes the dialog to select the note and
123     * the note layer to draw the selected note's comments.
124     * @param note Selected note. Null indicates no selection
125     */
126    public void setSelectedNote(Note note) {
127        selectedNote = note;
128        if (Main.map != null) {
129            Main.map.noteDialog.selectionChanged();
130            Main.map.mapView.repaint();
131        }
132    }
133
134    /**
135     * Return whether or not there are any changes in the note data set.
136     * These changes may need to be either uploaded or saved.
137     * @return true if local modifications have been made to the note data set. False otherwise.
138     */
139    public synchronized boolean isModified() {
140        for (Note note : noteList) {
141            if (note.getId() < 0) { //notes with negative IDs are new
142                return true;
143            }
144            for (NoteComment comment : note.getComments()) {
145                if (comment.getIsNew()) {
146                    return true;
147                }
148            }
149        }
150        return false;
151    }
152
153    /**
154     * Add notes to the data set. It only adds a note if the ID is not already present
155     * @param newNotes A list of notes to add
156     */
157    public synchronized void addNotes(List<Note> newNotes) {
158        for (Note newNote : newNotes) {
159            if (!noteList.contains(newNote)) {
160                noteList.add(newNote);
161            }
162            if (newNote.getId() <= newNoteId) {
163                newNoteId = newNote.getId() - 1;
164            }
165        }
166        dataUpdated();
167        if (Main.isDebugEnabled()) {
168            Main.debug("notes in current set: " + noteList.size());
169        }
170    }
171
172    /**
173     * Create a new note
174     * @param location Location of note
175     * @param text Required comment with which to open the note
176     */
177    public synchronized void createNote(LatLon location, String text) {
178        if(text == null || text.isEmpty()) {
179            throw new IllegalArgumentException("Comment can not be blank when creating a note");
180        }
181        Note note = new Note(location);
182        note.setCreatedAt(new Date());
183        note.setState(State.open);
184        note.setId(newNoteId--);
185        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.opened, true);
186        note.addComment(comment);
187        if (Main.isDebugEnabled()) {
188            Main.debug("Created note {0} with comment: {1}", note.getId(), text);
189        }
190        noteList.add(note);
191        dataUpdated();
192    }
193
194    /**
195     * Add a new comment to an existing note
196     * @param note Note to add comment to. Must already exist in the layer
197     * @param text Comment to add
198     */
199    public synchronized void addCommentToNote(Note note, String text) {
200        if (!noteList.contains(note)) {
201            throw new IllegalArgumentException("Note to modify must be in layer");
202        }
203        if (note.getState() == State.closed) {
204            throw new IllegalStateException("Cannot add a comment to a closed note");
205        }
206        if (Main.isDebugEnabled()) {
207            Main.debug("Adding comment to note {0}: {1}", note.getId(), text);
208        }
209        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.commented, true);
210        note.addComment(comment);
211        dataUpdated();
212    }
213
214    /**
215     * Close note with comment
216     * @param note Note to close. Must already exist in the layer
217     * @param text Comment to attach to close action, if desired
218     */
219    public synchronized void closeNote(Note note, String text) {
220        if (!noteList.contains(note)) {
221            throw new IllegalArgumentException("Note to close must be in layer");
222        }
223        if (note.getState() != State.open) {
224            throw new IllegalStateException("Cannot close a note that isn't open");
225        }
226        if (Main.isDebugEnabled()) {
227            Main.debug("closing note {0} with comment: {1}", note.getId(), text);
228        }
229        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.closed, true);
230        note.addComment(comment);
231        note.setState(State.closed);
232        note.setClosedAt(new Date());
233        dataUpdated();
234    }
235
236    /**
237     * Reopen a closed note.
238     * @param note Note to reopen. Must already exist in the layer
239     * @param text Comment to attach to the reopen action, if desired
240     */
241    public synchronized void reOpenNote(Note note, String text) {
242        if (!noteList.contains(note)) {
243            throw new IllegalArgumentException("Note to reopen must be in layer");
244        }
245        if (note.getState() != State.closed) {
246            throw new IllegalStateException("Cannot reopen a note that isn't closed");
247        }
248        if (Main.isDebugEnabled()) {
249            Main.debug("reopening note {0} with comment: {1}", note.getId(), text);
250        }
251        NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.reopened, true);
252        note.addComment(comment);
253        note.setState(State.open);
254        dataUpdated();
255    }
256
257    private void dataUpdated() {
258        Collections.sort(noteList, comparator);
259        Main.map.noteDialog.setNoteList(noteList);
260        Main.map.mapView.repaint();
261    }
262
263    private User getCurrentUser() {
264        JosmUserIdentityManager userMgr = JosmUserIdentityManager.getInstance();
265        return User.createOsmUser(userMgr.getUserId(), userMgr.getUserName());
266    }
267
268    /**
269     * Updates notes with new state. Primarily to be used when updating the
270     * note layer after uploading note changes to the server.
271     * @param updatedNotes Map containing the original note as the key and the updated note as the value
272     */
273    public synchronized void updateNotes(Map<Note, Note> updatedNotes) {
274        for (Map.Entry<Note, Note> entry : updatedNotes.entrySet()) {
275            Note oldNote = entry.getKey();
276            Note newNote = entry.getValue();
277            oldNote.updateWith(newNote);
278        }
279        dataUpdated();
280    }
281
282    /** @return The current comparator being used to sort the note list */
283    public Comparator<Note> getCurrentSortMethod() {
284        return comparator;
285    }
286
287    /** Set the comparator to be used to sort the note list. Several are available
288     * as public static members of this class.
289     * @param comparator - The Note comparator to sort by
290     */
291    public void setSortMethod(Comparator<Note> comparator) {
292        this.comparator = comparator;
293        dataUpdated();
294    }
295}