Index: src/org/openstreetmap/josm/actions/UploadNotesAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/UploadNotesAction.java	(revision 0)
+++ src/org/openstreetmap/josm/actions/UploadNotesAction.java	(working copy)
@@ -0,0 +1,54 @@
+// 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;
+        }
+        Main.debug("uploading note changes");
+        NoteData noteData = layer.getNoteData();
+
+        if(noteData == null || !noteData.isModified()) {
+            Main.debug("No changed notes to upload");
+            return;
+        }
+        UploadNotesTask uploadTask = new UploadNotesTask();
+        uploadTask.uploadNotes(noteData, new PleaseWaitProgressMonitor(tr("Uploading notes to server")));
+    }
+}
Index: src/org/openstreetmap/josm/actions/upload/UploadNotesTask.java
===================================================================
--- src/org/openstreetmap/josm/actions/upload/UploadNotesTask.java	(revision 0)
+++ src/org/openstreetmap/josm/actions/upload/UploadNotesTask.java	(working copy)
@@ -0,0 +1,121 @@
+// 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("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() {
+            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()) {
+                        Main.debug("found note change to upload");
+                        try {
+                            Note newNote;
+                            switch (comment.getNoteAction()) {
+                            case opened:
+                                Main.debug("opening new note");
+                                newNote = api.createNote(note.getLatLon(), comment.getText(), monitor);
+                                note.setId(newNote.getId());
+                                break;
+                            case closed:
+                                Main.debug("closing note " + note.getId());
+                                newNote = api.closeNote(note, comment.getText(), monitor);
+                                break;
+                            case commented:
+                                Main.debug("adding comment to note " + note.getId());
+                                newNote = api.addCommentToNote(note, comment.getText(), monitor);
+                                break;
+                            case reopened:
+                                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() {
+            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: src/org/openstreetmap/josm/data/osm/NoteData.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/NoteData.java	(revision 7698)
+++ src/org/openstreetmap/josm/data/osm/NoteData.java	(working copy)
@@ -4,6 +4,7 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.coor.LatLon;
@@ -63,10 +64,29 @@
     }
 
     /**
+     * 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;
+    }
+
+    /**
      * Add notes to the data set. It only adds a note if the ID is not already present
      * @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)) {
                 noteList.add(newNote);
@@ -84,7 +104,7 @@
      * @param location Location of note
      * @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");
         }
@@ -104,7 +124,7 @@
      * @param note Note to add comment to. Must already exist in the layer
      * @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");
         }
@@ -122,7 +142,7 @@
      * @param note Note to close. Must already exist in the layer
      * @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");
         }
@@ -142,7 +162,7 @@
      * @param note Note to reopen. Must already exist in the layer
      * @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");
         }
@@ -165,4 +185,18 @@
         JosmUserIdentityManager userMgr = JosmUserIdentityManager.getInstance();
         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: src/org/openstreetmap/josm/gui/dialogs/NoteDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/NoteDialog.java	(revision 7698)
+++ src/org/openstreetmap/josm/gui/dialogs/NoteDialog.java	(working copy)
@@ -27,6 +27,7 @@
 import javax.swing.event.ListSelectionListener;
 
 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;
 import org.openstreetmap.josm.data.notes.Note.State;
@@ -72,6 +73,7 @@
     private final CloseAction closeAction;
     private final NewAction newAction;
     private final ReopenAction reopenAction;
+    private final UploadNotesAction uploadAction;
 
     private NoteData noteData;
 
@@ -84,6 +86,7 @@
         closeAction = new CloseAction();
         newAction = new NewAction();
         reopenAction = new ReopenAction();
+        uploadAction = new UploadNotesAction();
         buildDialog();
     }
 
@@ -113,7 +116,8 @@
                 new SideButton(newAction, false),
                 new SideButton(addCommentAction, false),
                 new SideButton(closeAction, false),
-                new SideButton(reopenAction, false)}));
+                new SideButton(reopenAction, false),
+                new SideButton(uploadAction, false)}));
         updateButtonStates();
     }
 
@@ -131,6 +135,11 @@
             addCommentAction.setEnabled(false);
             reopenAction.setEnabled(true);
         }
+        if(noteData == null || !noteData.isModified()) {
+            uploadAction.setEnabled(false);
+        } else {
+            uploadAction.setEnabled(true);
+        }
     }
 
     @Override
Index: src/org/openstreetmap/josm/gui/layer/NoteLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/NoteLayer.java	(revision 7698)
+++ src/org/openstreetmap/josm/gui/layer/NoteLayer.java	(working copy)
@@ -72,17 +72,7 @@
 
     @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();
     }
 
     @Override
