Index: trunk/src/org/openstreetmap/josm/actions/UploadNotesAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/UploadNotesAction.java	(revision 7699)
+++ trunk/src/org/openstreetmap/josm/actions/UploadNotesAction.java	(revision 7699)
@@ -0,0 +1,58 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.actions;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.upload.UploadNotesTask;
+import org.openstreetmap.josm.data.osm.NoteData;
+import org.openstreetmap.josm.gui.layer.NoteLayer;
+import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Action to initiate uploading changed notes to the OSM server.
+ * On click, it finds the note layer and fires off an upload task
+ * with the note data contained in the layer.
+ *
+ */
+public class UploadNotesAction extends JosmAction {
+
+    /** Create a new action to upload notes */
+    public UploadNotesAction () {
+        putValue(SHORT_DESCRIPTION,tr("Upload note changes to server"));
+        putValue(NAME, tr("Upload notes"));
+        putValue(SMALL_ICON, ImageProvider.get("upload"));
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        List<NoteLayer> noteLayers = null;
+        if (Main.map != null) {
+            noteLayers = Main.map.mapView.getLayersOfType(NoteLayer.class);
+        }
+        NoteLayer layer;
+        if (noteLayers != null && noteLayers.size() > 0) {
+            layer = noteLayers.get(0);
+        } else {
+            Main.error("No note layer found");
+            return;
+        }
+        if (Main.isDebugEnabled()) {
+            Main.debug("uploading note changes");
+        }
+        NoteData noteData = layer.getNoteData();
+
+        if(noteData == null || !noteData.isModified()) {
+            if (Main.isDebugEnabled()) {
+                Main.debug("No changed notes to upload");
+            }
+            return;
+        }
+        UploadNotesTask uploadTask = new UploadNotesTask();
+        uploadTask.uploadNotes(noteData, new PleaseWaitProgressMonitor(tr("Uploading notes to server")));
+    }
+}
Index: trunk/src/org/openstreetmap/josm/actions/upload/UploadNotesTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/upload/UploadNotesTask.java	(revision 7699)
+++ trunk/src/org/openstreetmap/josm/actions/upload/UploadNotesTask.java	(revision 7699)
@@ -0,0 +1,135 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.actions.upload;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.notes.Note;
+import org.openstreetmap.josm.data.notes.NoteComment;
+import org.openstreetmap.josm.data.osm.NoteData;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmApi;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.xml.sax.SAXException;
+
+/**
+ * Class for uploading note changes to the server
+ */
+public class UploadNotesTask {
+
+    private UploadTask uploadTask;
+    private NoteData noteData;
+
+    /**
+     * Upload notes with modifications to the server
+     * @param noteData Note dataset with changes to upload
+     * @param progressMonitor progress monitor for user feedback
+     */
+    public void uploadNotes(NoteData noteData, ProgressMonitor progressMonitor) {
+        this.noteData = noteData;
+        uploadTask = new UploadTask(tr("Uploading modified notes"), progressMonitor);
+        Main.worker.submit(uploadTask);
+    }
+
+    private class UploadTask extends PleaseWaitRunnable {
+
+        private boolean isCanceled = false;
+        Map<Note, Note> updatedNotes = new HashMap<>();
+        Map<Note, Exception> failedNotes = new HashMap<>();
+
+        public UploadTask(String title, ProgressMonitor monitor) {
+            super(title, monitor, false);
+        }
+
+        @Override
+        protected void cancel() {
+            if (Main.isDebugEnabled()) {
+                Main.debug("note upload canceled");
+            }
+            isCanceled = true;
+        }
+
+        @Override
+        protected void realRun() throws SAXException, IOException, OsmTransferException {
+            ProgressMonitor monitor = progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
+            OsmApi api = OsmApi.getOsmApi();
+            for (Note note : noteData.getNotes()) {
+                if(isCanceled) {
+                    Main.info("Note upload interrupted by user");
+                    break;
+                }
+                for (NoteComment comment : note.getComments()) {
+                    if (comment.getIsNew()) {
+                        if (Main.isDebugEnabled()) {
+                            Main.debug("found note change to upload");
+                        }
+                        try {
+                            Note newNote;
+                            switch (comment.getNoteAction()) {
+                            case opened:
+                                if (Main.isDebugEnabled()) {
+                                    Main.debug("opening new note");
+                                }
+                                newNote = api.createNote(note.getLatLon(), comment.getText(), monitor);
+                                note.setId(newNote.getId());
+                                break;
+                            case closed:
+                                if (Main.isDebugEnabled()) {
+                                    Main.debug("closing note " + note.getId());
+                                }
+                                newNote = api.closeNote(note, comment.getText(), monitor);
+                                break;
+                            case commented:
+                                if (Main.isDebugEnabled()) {
+                                    Main.debug("adding comment to note " + note.getId());
+                                }
+                                newNote = api.addCommentToNote(note, comment.getText(), monitor);
+                                break;
+                            case reopened:
+                                if (Main.isDebugEnabled()) {
+                                    Main.debug("reopening note " + note.getId());
+                                }
+                                newNote = api.reopenNote(note, comment.getText(), monitor);
+                                break;
+                            default:
+                                newNote = null;
+                            }
+                            updatedNotes.put(note, newNote);
+                        } catch (Exception e) {
+                            Main.error("Failed to upload note to server: " + note.getId());
+                            failedNotes.put(note, e);
+                        }
+                    }
+                }
+            }
+        }
+
+        /** Updates the note layer with uploaded notes and notifies the user of any upload failures */
+        @Override
+        protected void finish() {
+            if (Main.isDebugEnabled()) {
+                Main.debug("finish called in notes upload task. Notes to update: " + updatedNotes.size());
+            }
+            noteData.updateNotes(updatedNotes);
+            if (!failedNotes.isEmpty()) {
+                Main.error("Some notes failed to upload");
+                StringBuilder sb = new StringBuilder();
+                for (Map.Entry<Note, Exception> entry : failedNotes.entrySet()) {
+                    sb.append(tr("Note {0} failed: {1}", entry.getKey().getId(), entry.getValue().getMessage()));
+                    sb.append("\n");
+                }
+                Main.error("Notes failed to upload: " + sb.toString());
+                JOptionPane.showMessageDialog(Main.map, sb.toString(), tr("Notes failed to upload"), JOptionPane.ERROR_MESSAGE);
+            }
+        }
+
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/data/osm/NoteData.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/NoteData.java	(revision 7698)
+++ trunk/src/org/openstreetmap/josm/data/osm/NoteData.java	(revision 7699)
@@ -5,4 +5,5 @@
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 import org.openstreetmap.josm.Main;
@@ -59,6 +60,27 @@
     public void setSelectedNote(Note note) {
         selectedNote = note;
-        Main.map.noteDialog.selectionChanged();
-        Main.map.mapView.repaint();
+        if (Main.map != null) {
+            Main.map.noteDialog.selectionChanged();
+            Main.map.mapView.repaint();
+        }
+    }
+
+    /**
+     * Return whether or not there are any changes in the note data set.
+     * These changes may need to be either uploaded or saved.
+     * @return true if local modifications have been made to the note data set. False otherwise.
+     */
+    public synchronized boolean isModified() {
+        for (Note note : noteList) {
+            if (note.getId() < 0) { //notes with negative IDs are new
+                return true;
+            }
+            for (NoteComment comment : note.getComments()) {
+                if (comment.getIsNew()) {
+                    return true;
+                }
+            }
+        }
+        return false;
     }
 
@@ -67,5 +89,5 @@
      * @param newNotes A list of notes to add
      */
-    public void addNotes(List<Note> newNotes) {
+    public synchronized void addNotes(List<Note> newNotes) {
         for (Note newNote : newNotes) {
             if (!noteList.contains(newNote)) {
@@ -77,5 +99,7 @@
         }
         dataUpdated();
-        Main.debug("notes in current set: " + noteList.size());
+        if (Main.isDebugEnabled()) {
+            Main.debug("notes in current set: " + noteList.size());
+        }
     }
 
@@ -85,5 +109,5 @@
      * @param text Required comment with which to open the note
      */
-    public void createNote(LatLon location, String text) {
+    public synchronized void createNote(LatLon location, String text) {
         if(text == null || text.isEmpty()) {
             throw new IllegalArgumentException("Comment can not be blank when creating a note");
@@ -95,5 +119,7 @@
         NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.opened, true);
         note.addComment(comment);
-        Main.debug("Created note {0} with comment: {1}", note.getId(), text);
+        if (Main.isDebugEnabled()) {
+            Main.debug("Created note {0} with comment: {1}", note.getId(), text);
+        }
         noteList.add(note);
         dataUpdated();
@@ -105,5 +131,5 @@
      * @param text Comment to add
      */
-    public void addCommentToNote(Note note, String text) {
+    public synchronized void addCommentToNote(Note note, String text) {
         if (!noteList.contains(note)) {
             throw new IllegalArgumentException("Note to modify must be in layer");
@@ -112,5 +138,7 @@
             throw new IllegalStateException("Cannot add a comment to a closed note");
         }
-        Main.debug("Adding comment to note {0}: {1}", note.getId(), text);
+        if (Main.isDebugEnabled()) {
+            Main.debug("Adding comment to note {0}: {1}", note.getId(), text);
+        }
         NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.commented, true);
         note.addComment(comment);
@@ -123,5 +151,5 @@
      * @param text Comment to attach to close action, if desired
      */
-    public void closeNote(Note note, String text) {
+    public synchronized void closeNote(Note note, String text) {
         if (!noteList.contains(note)) {
             throw new IllegalArgumentException("Note to close must be in layer");
@@ -130,5 +158,7 @@
             throw new IllegalStateException("Cannot close a note that isn't open");
         }
-        Main.debug("closing note {0} with comment: {1}", note.getId(), text);
+        if (Main.isDebugEnabled()) {
+            Main.debug("closing note {0} with comment: {1}", note.getId(), text);
+        }
         NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.closed, true);
         note.addComment(comment);
@@ -143,5 +173,5 @@
      * @param text Comment to attach to the reopen action, if desired
      */
-    public void reOpenNote(Note note, String text) {
+    public synchronized void reOpenNote(Note note, String text) {
         if (!noteList.contains(note)) {
             throw new IllegalArgumentException("Note to reopen must be in layer");
@@ -150,5 +180,7 @@
             throw new IllegalStateException("Cannot reopen a note that isn't closed");
         }
-        Main.debug("reopening note {0} with comment: {1}", note.getId(), text);
+        if (Main.isDebugEnabled()) {
+            Main.debug("reopening note {0} with comment: {1}", note.getId(), text);
+        }
         NoteComment comment = new NoteComment(new Date(), getCurrentUser(), text, NoteComment.Action.reopened, true);
         note.addComment(comment);
@@ -166,3 +198,17 @@
         return User.createOsmUser(userMgr.getUserId(), userMgr.getUserName());
     }
+
+    /**
+     * Updates notes with new state. Primarily to be used when updating the
+     * note layer after uploading note changes to the server.
+     * @param updatedNotes Map containing the original note as the key and the updated note as the value
+     */
+    public synchronized void updateNotes(Map<Note, Note> updatedNotes) {
+        for (Map.Entry<Note, Note> entry : updatedNotes.entrySet()) {
+            Note oldNote = entry.getKey();
+            Note newNote = entry.getValue();
+            oldNote.updateWith(newNote);
+        }
+        dataUpdated();
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/NoteDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/NoteDialog.java	(revision 7698)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/NoteDialog.java	(revision 7699)
@@ -28,4 +28,5 @@
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.UploadNotesAction;
 import org.openstreetmap.josm.actions.mapmode.AddNoteAction;
 import org.openstreetmap.josm.data.notes.Note;
@@ -73,4 +74,5 @@
     private final NewAction newAction;
     private final ReopenAction reopenAction;
+    private final UploadNotesAction uploadAction;
 
     private NoteData noteData;
@@ -79,5 +81,7 @@
     public NoteDialog() {
         super("Notes", "notes/note_open.png", "List of notes", null, 150);
-        Main.debug("constructed note dialog");
+        if (Main.isDebugEnabled()) {
+            Main.debug("constructed note dialog");
+        }
 
         addCommentAction = new AddCommentAction();
@@ -85,4 +89,5 @@
         newAction = new NewAction();
         reopenAction = new ReopenAction();
+        uploadAction = new UploadNotesAction();
         buildDialog();
     }
@@ -114,5 +119,6 @@
                 new SideButton(addCommentAction, false),
                 new SideButton(closeAction, false),
-                new SideButton(reopenAction, false)}));
+                new SideButton(reopenAction, false),
+                new SideButton(uploadAction, false)}));
         updateButtonStates();
     }
@@ -132,4 +138,9 @@
             reopenAction.setEnabled(true);
         }
+        if(noteData == null || !noteData.isModified()) {
+            uploadAction.setEnabled(false);
+        } else {
+            uploadAction.setEnabled(true);
+        }
     }
 
@@ -149,7 +160,11 @@
     @Override
     public void layerAdded(Layer newLayer) {
-        Main.debug("layer added: " + newLayer);
+        if (Main.isDebugEnabled()) {
+            Main.debug("layer added: " + newLayer);
+        }
         if (newLayer instanceof NoteLayer) {
-            Main.debug("note layer added");
+            if (Main.isDebugEnabled()) {
+                Main.debug("note layer added");
+            }
             noteData = ((NoteLayer)newLayer).getNoteData();
             model.setData(noteData.getNotes());
@@ -160,5 +175,7 @@
     public void layerRemoved(Layer oldLayer) {
         if (oldLayer instanceof NoteLayer) {
-            Main.debug("note layer removed. Clearing everything");
+            if (Main.isDebugEnabled()) {
+                Main.debug("note layer removed. Clearing everything");
+            }
             noteData = null;
             model.clearData();
Index: trunk/src/org/openstreetmap/josm/gui/layer/NoteLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/NoteLayer.java	(revision 7698)
+++ trunk/src/org/openstreetmap/josm/gui/layer/NoteLayer.java	(revision 7699)
@@ -73,15 +73,5 @@
     @Override
     public boolean isModified() {
-        for (Note note : noteData.getNotes()) {
-            if (note.getId() < 0) { //notes with negative IDs are new
-                return true;
-            }
-            for (NoteComment comment : note.getComments()) {
-                if (comment.getIsNew()) {
-                    return true;
-                }
-            }
-        }
-        return false;
+        return noteData.isModified();
     }
 
