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