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