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}