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