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