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.Main; 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.gui.JosmUserIdentityManager; 018 019/** 020 * Class to hold and perform operations on a set of notes 021 */ 022public class NoteData { 023 024 private long newNoteId = -1; 025 026 private final Storage<Note> noteList; 027 private Note selectedNote; 028 private Comparator<Note> comparator = DEFAULT_COMPARATOR; 029 030 /** 031 * Sorts notes in the following order: 032 * 1) Open notes 033 * 2) Closed notes 034 * 3) New notes 035 * Within each subgroup it sorts by ID 036 */ 037 public static final Comparator<Note> DEFAULT_COMPARATOR = (n1, n2) -> { 038 if (n1.getId() < 0 && n2.getId() > 0) { 039 return 1; 040 } 041 if (n1.getId() > 0 && n2.getId() < 0) { 042 return -1; 043 } 044 if (n1.getState() == State.CLOSED && n2.getState() == State.OPEN) { 045 return 1; 046 } 047 if (n1.getState() == State.OPEN && n2.getState() == State.CLOSED) { 048 return -1; 049 } 050 return Long.compare(Math.abs(n1.getId()), Math.abs(n2.getId())); 051 }; 052 053 /** Sorts notes strictly by creation date */ 054 public static final Comparator<Note> DATE_COMPARATOR = (n1, n2) -> n1.getCreatedAt().compareTo(n2.getCreatedAt()); 055 056 /** Sorts notes by user, then creation date */ 057 public static final Comparator<Note> USER_COMPARATOR = (n1, n2) -> { 058 String n1User = n1.getFirstComment().getUser().getName(); 059 String n2User = n2.getFirstComment().getUser().getName(); 060 if (n1User.equals(n2User)) { 061 return n1.getCreatedAt().compareTo(n2.getCreatedAt()); 062 } 063 return n1.getFirstComment().getUser().getName().compareTo(n2.getFirstComment().getUser().getName()); 064 }; 065 066 /** Sorts notes by the last modified date */ 067 public static final Comparator<Note> LAST_ACTION_COMPARATOR = (n1, n2) -> { 068 Date n1Date = n1.getComments().get(n1.getComments().size()-1).getCommentTimestamp(); 069 Date n2Date = n2.getComments().get(n2.getComments().size()-1).getCommentTimestamp(); 070 return n1Date.compareTo(n2Date); 071 }; 072 073 /** 074 * Construct a new note container with a given list of notes 075 * @param notes The list of notes to populate the container with 076 */ 077 public NoteData(Collection<Note> notes) { 078 noteList = new Storage<>(); 079 if (notes != null) { 080 for (Note note : notes) { 081 noteList.add(note); 082 if (note.getId() <= newNoteId) { 083 newNoteId = note.getId() - 1; 084 } 085 } 086 } 087 } 088 089 /** 090 * Returns the notes stored in this layer 091 * @return collection of notes 092 */ 093 public Collection<Note> getNotes() { 094 return Collections.unmodifiableCollection(noteList); 095 } 096 097 /** 098 * Returns the notes stored in this layer sorted according to {@link #comparator} 099 * @return sorted collection of notes 100 */ 101 public Collection<Note> getSortedNotes() { 102 final List<Note> list = new ArrayList<>(noteList); 103 list.sort(comparator); 104 return list; 105 } 106 107 /** Returns the currently selected note 108 * @return currently selected note 109 */ 110 public Note getSelectedNote() { 111 return selectedNote; 112 } 113 114 /** Set a selected note. Causes the dialog to select the note and 115 * the note layer to draw the selected note's comments. 116 * @param note Selected note. Null indicates no selection 117 */ 118 public void setSelectedNote(Note note) { 119 selectedNote = note; 120 if (Main.map != null) { 121 Main.map.noteDialog.selectionChanged(); 122 Main.map.mapView.repaint(); 123 } 124 } 125 126 /** 127 * Return whether or not there are any changes in the note data set. 128 * These changes may need to be either uploaded or saved. 129 * @return true if local modifications have been made to the note data set. False otherwise. 130 */ 131 public synchronized boolean isModified() { 132 for (Note note : noteList) { 133 if (note.getId() < 0) { //notes with negative IDs are new 134 return true; 135 } 136 for (NoteComment comment : note.getComments()) { 137 if (comment.isNew()) { 138 return true; 139 } 140 } 141 } 142 return false; 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 Main.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 (text == null || text.isEmpty()) { 177 throw new IllegalArgumentException("Comment can not be blank when creating a note"); 178 } 179 Note note = new Note(location); 180 note.setCreatedAt(new Date()); 181 note.setState(State.OPEN); 182 note.setId(newNoteId--); 183 NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.OPENED, true); 184 note.addComment(comment); 185 if (Main.isDebugEnabled()) { 186 Main.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 (Main.isDebugEnabled()) { 205 Main.debug("Adding comment to note {0}: {1}", note.getId(), text); 206 } 207 NoteComment comment = new NoteComment(new Date(), 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 (Main.isDebugEnabled()) { 225 Main.debug("closing note {0} with comment: {1}", note.getId(), text); 226 } 227 NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.CLOSED, true); 228 note.addComment(comment); 229 note.setState(State.CLOSED); 230 note.setClosedAt(new Date()); 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 if (Main.isDebugEnabled()) { 247 Main.debug("reopening note {0} with comment: {1}", note.getId(), text); 248 } 249 NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.REOPENED, true); 250 note.addComment(comment); 251 note.setState(State.OPEN); 252 dataUpdated(); 253 } 254 255 private void dataUpdated() { 256 if (Main.isDisplayingMapView()) { 257 Main.map.noteDialog.setNotes(getSortedNotes()); 258 Main.map.mapView.repaint(); 259 } 260 } 261 262 private static User getCurrentUser() { 263 JosmUserIdentityManager userMgr = JosmUserIdentityManager.getInstance(); 264 return User.createOsmUser(userMgr.getUserId(), userMgr.getUserName()); 265 } 266 267 /** 268 * Updates notes with new state. Primarily to be used when updating the 269 * note layer after uploading note changes to the server. 270 * @param updatedNotes Map containing the original note as the key and the updated note as the value 271 */ 272 public synchronized void updateNotes(Map<Note, Note> updatedNotes) { 273 for (Map.Entry<Note, Note> entry : updatedNotes.entrySet()) { 274 Note oldNote = entry.getKey(); 275 Note newNote = entry.getValue(); 276 boolean reindex = oldNote.hashCode() != newNote.hashCode(); 277 if (reindex) { 278 noteList.removeElem(oldNote); 279 } 280 oldNote.updateWith(newNote); 281 if (reindex) { 282 noteList.add(oldNote); 283 } 284 } 285 dataUpdated(); 286 } 287 288 /** @return The current comparator being used to sort the note list */ 289 public Comparator<Note> getCurrentSortMethod() { 290 return comparator; 291 } 292 293 /** Set the comparator to be used to sort the note list. Several are available 294 * as public static members of this class. 295 * @param comparator - The Note comparator to sort by 296 */ 297 public void setSortMethod(Comparator<Note> comparator) { 298 this.comparator = comparator; 299 dataUpdated(); 300 } 301}