Index: trunk/src/org/openstreetmap/josm/Main.java
===================================================================
--- trunk/src/org/openstreetmap/josm/Main.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/Main.java	(revision 7358)
@@ -77,4 +77,5 @@
 import org.openstreetmap.josm.gui.io.SaveLayersDialog;
 import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.ModifiableLayer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
@@ -890,5 +891,5 @@
 
     /**
-     * Asks user to perform "save layer" operations (save .osm on disk and/or upload osm data to server) for all {@link OsmDataLayer} before JOSM exits.
+     * Asks user to perform "save layer" operations (save on disk and/or upload data to server) for all {@link ModifiableLayer} before JOSM exits.
      * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
      * @since 2025
@@ -896,11 +897,11 @@
     public static boolean saveUnsavedModifications() {
         if (!isDisplayingMapView()) return true;
-        return saveUnsavedModifications(map.mapView.getLayersOfType(OsmDataLayer.class), true);
-    }
-
-    /**
-     * Asks user to perform "save layer" operations (save .osm on disk and/or upload osm data to server) before osm layers deletion.
-     *
-     * @param selectedLayers The layers to check. Only instances of {@link OsmDataLayer} are considered.
+        return saveUnsavedModifications(map.mapView.getLayersOfType(ModifiableLayer.class), true);
+    }
+
+    /**
+     * Asks user to perform "save layer" operations (save on disk and/or upload data to server) before data layers deletion.
+     *
+     * @param selectedLayers The layers to check. Only instances of {@link ModifiableLayer} are considered.
      * @param exit {@code true} if JOSM is exiting, {@code false} otherwise.
      * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
@@ -909,11 +910,11 @@
     public static boolean saveUnsavedModifications(Iterable<? extends Layer> selectedLayers, boolean exit) {
         SaveLayersDialog dialog = new SaveLayersDialog(parent);
-        List<OsmDataLayer> layersWithUnmodifiedChanges = new ArrayList<>();
+        List<ModifiableLayer> layersWithUnmodifiedChanges = new ArrayList<>();
         for (Layer l: selectedLayers) {
-            if (!(l instanceof OsmDataLayer)) {
+            if (!(l instanceof ModifiableLayer)) {
                 continue;
             }
-            OsmDataLayer odl = (OsmDataLayer)l;
-            if ((odl.requiresSaveToFile() || (odl.requiresUploadToServer() && !odl.isUploadDiscouraged())) && odl.data.isModified()) {
+            ModifiableLayer odl = (ModifiableLayer)l;
+            if ((odl.requiresSaveToFile() || (odl.requiresUploadToServer() && !odl.isUploadDiscouraged())) && odl.isModified()) {
                 layersWithUnmodifiedChanges.add(odl);
             }
Index: trunk/src/org/openstreetmap/josm/actions/UploadAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/UploadAction.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/actions/UploadAction.java	(revision 7358)
@@ -27,4 +27,5 @@
 import org.openstreetmap.josm.gui.io.UploadDialog;
 import org.openstreetmap.josm.gui.io.UploadPrimitivesTask;
+import org.openstreetmap.josm.gui.layer.ModifiableLayer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
@@ -142,6 +143,7 @@
     }
 
-    public boolean checkPreUploadConditions(OsmDataLayer layer) {
-        return checkPreUploadConditions(layer, new APIDataSet(layer.data));
+    public static boolean checkPreUploadConditions(ModifiableLayer layer) {
+        return checkPreUploadConditions(layer,
+                layer instanceof OsmDataLayer ? new APIDataSet(((OsmDataLayer)layer).data) : null);
     }
 
@@ -162,5 +164,5 @@
      * want to continue
      */
-    public static boolean warnUploadDiscouraged(OsmDataLayer layer) {
+    public static boolean warnUploadDiscouraged(ModifiableLayer layer) {
         return GuiHelper.warnUser(tr("Upload discouraged"),
                 "<html>" +
@@ -182,5 +184,5 @@
      * @return true, if the preconditions are met; false, otherwise
      */
-    public boolean checkPreUploadConditions(OsmDataLayer layer, APIDataSet apiData) {
+    public static boolean checkPreUploadConditions(ModifiableLayer layer, APIDataSet apiData) {
         if (layer.isUploadDiscouraged()) {
             if (warnUploadDiscouraged(layer)) {
@@ -188,15 +190,20 @@
             }
         }
-        ConflictCollection conflicts = layer.getConflicts();
-        if (apiData.participatesInConflict(conflicts)) {
-            alertUnresolvedConflicts(layer);
-            return false;
+        if (layer instanceof OsmDataLayer) {
+            OsmDataLayer osmLayer = (OsmDataLayer) layer;
+            ConflictCollection conflicts = osmLayer.getConflicts();
+            if (apiData.participatesInConflict(conflicts)) {
+                alertUnresolvedConflicts(osmLayer);
+                return false;
+            }
         }
         // Call all upload hooks in sequence.
         // FIXME: this should become an asynchronous task
         //
-        for (UploadHook hook : uploadHooks) {
-            if (!hook.checkUpload(apiData))
-                return false;
+        if (apiData != null) {
+            for (UploadHook hook : uploadHooks) {
+                if (!hook.checkUpload(apiData))
+                    return false;
+            }
         }
 
Index: trunk/src/org/openstreetmap/josm/gui/io/AbstractIOTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/AbstractIOTask.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/io/AbstractIOTask.java	(revision 7358)
@@ -15,4 +15,7 @@
     private Exception lastException;
 
+    /**
+     * Contructs a new {@code AbstractIOTask}.
+     */
     public AbstractIOTask() {
         canceled = false;
Index: trunk/src/org/openstreetmap/josm/gui/io/AbstractUploadDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/AbstractUploadDialog.java	(revision 7358)
+++ trunk/src/org/openstreetmap/josm/gui/io/AbstractUploadDialog.java	(revision 7358)
@@ -0,0 +1,207 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import java.awt.GraphicsConfiguration;
+import java.awt.HeadlessException;
+import java.awt.Window;
+
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+
+/**
+ * This is an abstract base class for dialogs used for entering generic upload options.
+ * @since 7358
+ */
+public abstract class AbstractUploadDialog extends JDialog {
+
+    private boolean canceled = false;
+
+    /**
+     * Creates a dialog with an empty title and the specified modality and
+     * {@code Window} as its owner.
+     * <p>
+     * This constructor sets the component's locale property to the value
+     * returned by {@code JComponent.getDefaultLocale}.
+     *
+     * @param owner the {@code Window} from which the dialog is displayed or
+     *     {@code null} if this dialog has no owner
+     * @param modalityType specifies whether dialog blocks input to other
+     *     windows when shown. {@code null} value and unsupported modality
+     *     types are equivalent to {@code MODELESS}
+     *
+     * @throws IllegalArgumentException
+     *     if the {@code owner} is not an instance of {@link java.awt.Dialog Dialog}
+     *     or {@link java.awt.Frame Frame}
+     * @throws IllegalArgumentException
+     *     if the {@code owner}'s {@code GraphicsConfiguration} is not from a screen device
+     * @throws HeadlessException
+     *     when {@code GraphicsEnvironment.isHeadless()} returns {@code true}
+     * @throws SecurityException
+     *     if the calling thread does not have permission to create modal dialogs
+     *     with the given {@code modalityType}
+     *
+     * @see java.awt.Dialog.ModalityType
+     * @see java.awt.Dialog#setModal
+     * @see java.awt.Dialog#setModalityType
+     * @see java.awt.GraphicsEnvironment#isHeadless
+     * @see JComponent#getDefaultLocale
+     */
+    public AbstractUploadDialog(Window owner, ModalityType modalityType) {
+        super(owner, modalityType);
+    }
+
+    /**
+     * Creates a dialog with the specified title, owner {@code Window},
+     * modality and {@code GraphicsConfiguration}.
+     * <p>
+     * NOTE: Any popup components ({@code JComboBox},
+     * {@code JPopupMenu}, {@code JMenuBar})
+     * created within a modal dialog will be forced to be lightweight.
+     * <p>
+     * This constructor sets the component's locale property to the value
+     * returned by {@code JComponent.getDefaultLocale}.
+     *
+     * @param owner the {@code Window} from which the dialog is displayed or
+     *     {@code null} if this dialog has no owner
+     * @param title the {@code String} to display in the dialog's
+     *     title bar or {@code null} if the dialog has no title
+     * @param modalityType specifies whether dialog blocks input to other
+     *     windows when shown. {@code null} value and unsupported modality
+     *     types are equivalent to {@code MODELESS}
+     * @param gc the {@code GraphicsConfiguration} of the target screen device;
+     *     if {@code null}, the default system {@code GraphicsConfiguration}
+     *     is assumed
+     * @throws IllegalArgumentException
+     *     if the {@code owner} is not an instance of {@link java.awt.Dialog Dialog}
+     *     or {@link java.awt.Frame Frame}
+     * @throws IllegalArgumentException
+     *     if the {@code owner}'s {@code GraphicsConfiguration} is not from a screen device
+     * @throws HeadlessException
+     *     when {@code GraphicsEnvironment.isHeadless()} returns {@code true}
+     * @throws SecurityException
+     *     if the calling thread does not have permission to create modal dialogs
+     *     with the given {@code modalityType}
+     *
+     * @see java.awt.Dialog.ModalityType
+     * @see java.awt.Dialog#setModal
+     * @see java.awt.Dialog#setModalityType
+     * @see java.awt.GraphicsEnvironment#isHeadless
+     * @see JComponent#getDefaultLocale
+     */
+    public AbstractUploadDialog(Window owner, String title, ModalityType modalityType, GraphicsConfiguration gc) {
+        super(owner, title, modalityType, gc);
+    }
+
+    /**
+     * Creates a dialog with the specified title, owner {@code Window} and
+     * modality.
+     * <p>
+     * This constructor sets the component's locale property to the value
+     * returned by {@code JComponent.getDefaultLocale}.
+     *
+     * @param owner the {@code Window} from which the dialog is displayed or
+     *     {@code null} if this dialog has no owner
+     * @param title the {@code String} to display in the dialog's
+     *     title bar or {@code null} if the dialog has no title
+     * @param modalityType specifies whether dialog blocks input to other
+     *     windows when shown. {@code null} value and unsupported modality
+     *     types are equivalent to {@code MODELESS}
+     *
+     * @throws IllegalArgumentException
+     *     if the {@code owner} is not an instance of {@link java.awt.Dialog Dialog}
+     *     or {@link java.awt.Frame Frame}
+     * @throws IllegalArgumentException
+     *     if the {@code owner}'s {@code GraphicsConfiguration} is not from a screen device
+     * @throws HeadlessException
+     *     when {@code GraphicsEnvironment.isHeadless()} returns {@code true}
+     * @throws SecurityException
+     *     if the calling thread does not have permission to create modal dialogs
+     *     with the given {@code modalityType}
+     *
+     * @see java.awt.Dialog.ModalityType
+     * @see java.awt.Dialog#setModal
+     * @see java.awt.Dialog#setModalityType
+     * @see java.awt.GraphicsEnvironment#isHeadless
+     * @see JComponent#getDefaultLocale
+     */
+    public AbstractUploadDialog(Window owner, String title, ModalityType modalityType) {
+        super(owner, title, modalityType);
+    }
+
+    /**
+     * Creates a modeless dialog with the specified title and owner
+     * {@code Window}.
+     * <p>
+     * This constructor sets the component's locale property to the value
+     * returned by {@code JComponent.getDefaultLocale}.
+     *
+     * @param owner the {@code Window} from which the dialog is displayed or
+     *     {@code null} if this dialog has no owner
+     * @param title the {@code String} to display in the dialog's
+     *     title bar or {@code null} if the dialog has no title
+     *
+     * @throws IllegalArgumentException
+     *     if the {@code owner} is not an instance of {@link java.awt.Dialog Dialog}
+     *     or {@link java.awt.Frame Frame}
+     * @throws IllegalArgumentException
+     *     if the {@code owner}'s {@code GraphicsConfiguration} is not from a screen device
+     * @throws HeadlessException
+     *     when {@code GraphicsEnvironment.isHeadless()} returns {@code true}
+     *
+     * @see java.awt.GraphicsEnvironment#isHeadless
+     * @see JComponent#getDefaultLocale
+     */
+    public AbstractUploadDialog(Window owner, String title) {
+        super(owner, title);
+    }
+
+    /**
+     * Creates a modeless dialog with the specified {@code Window}
+     * as its owner and an empty title.
+     * <p>
+     * This constructor sets the component's locale property to the value
+     * returned by {@code JComponent.getDefaultLocale}.
+     *
+     * @param owner the {@code Window} from which the dialog is displayed or
+     *     {@code null} if this dialog has no owner
+     *
+     * @throws IllegalArgumentException
+     *     if the {@code owner} is not an instance of {@link java.awt.Dialog Dialog}
+     *     or {@link java.awt.Frame Frame}
+     * @throws IllegalArgumentException
+     *     if the {@code owner}'s {@code GraphicsConfiguration} is not from a screen device
+     * @throws HeadlessException
+     *     when {@code GraphicsEnvironment.isHeadless()} returns {@code true}
+     *
+     * @see java.awt.GraphicsEnvironment#isHeadless
+     * @see JComponent#getDefaultLocale
+     */
+    public AbstractUploadDialog(Window owner) {
+        super(owner);
+    }
+
+    /**
+     * Returns true if the dialog was canceled
+     *
+     * @return true if the dialog was canceled
+     */
+    public final boolean isCanceled() {
+        return canceled;
+    }
+
+    /**
+     * Sets whether the dialog was canceled
+     *
+     * @param canceled true if the dialog is canceled
+     */
+    protected void setCanceled(boolean canceled) {
+        this.canceled = canceled;
+    }
+
+    /**
+     * Remembers the user input in the preference settings
+     */
+    public void rememberUserInput() {
+        // Override if needed
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfo.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfo.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfo.java	(revision 7358)
@@ -4,5 +4,5 @@
 import java.io.File;
 
-import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.ModifiableLayer;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 
@@ -10,10 +10,10 @@
  * SaveLayerInfo represents the information, user preferences and save/upload states of
  * a layer which might be uploaded/saved.
- *
+ * @since 2025
  */
 class SaveLayerInfo implements Comparable<SaveLayerInfo> {
 
-    /** the osm data layer */
-    private OsmDataLayer layer;
+    /** the modifiable layer */
+    private ModifiableLayer layer;
     private boolean doCheckSaveConditions;
     private boolean doSaveToFile;
@@ -28,5 +28,5 @@
      * @throws IllegalArgumentException thrown if layer is null
      */
-    public SaveLayerInfo(OsmDataLayer layer) {
+    public SaveLayerInfo(ModifiableLayer layer) {
         CheckParameterUtil.ensureParameterNotNull(layer, "layer");
         this.layer = layer;
@@ -42,5 +42,5 @@
      * @return the layer this info objects holds information for
      */
-    public OsmDataLayer getLayer() {
+    public ModifiableLayer getLayer() {
         return layer;
     }
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayerTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayerTask.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayerTask.java	(revision 7358)
@@ -11,6 +11,6 @@
 
 /**
- * SaveLayerTask saves the data managed by an {@link org.openstreetmap.josm.gui.layer.OsmDataLayer} to the
- * {@link org.openstreetmap.josm.gui.layer.OsmDataLayer#getAssociatedFile()}.
+ * SaveLayerTask saves the data managed by an {@link org.openstreetmap.josm.gui.layer.ModifiableLayer} to the
+ * {@link org.openstreetmap.josm.gui.layer.Layer#getAssociatedFile()}.
  *
  * <pre>
@@ -26,5 +26,5 @@
  * </pre>
  */
-class SaveLayerTask extends AbstractIOTask {
+public class SaveLayerTask extends AbstractIOTask {
     private SaveLayerInfo layerInfo;
     private ProgressMonitor parentMonitor;
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java	(revision 7358)
@@ -44,7 +44,7 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.UploadAction;
-import org.openstreetmap.josm.data.APIDataSet;
 import org.openstreetmap.josm.gui.ExceptionDialogUtil;
 import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
+import org.openstreetmap.josm.gui.layer.ModifiableLayer;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
 import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
@@ -449,29 +449,31 @@
         protected void uploadLayers(List<SaveLayerInfo> toUpload) {
             for (final SaveLayerInfo layerInfo: toUpload) {
+                ModifiableLayer layer = layerInfo.getLayer();
                 if (canceled) {
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
+                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
                     continue;
                 }
                 monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
 
-                if (!new UploadAction().checkPreUploadConditions(layerInfo.getLayer())) {
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                if (!UploadAction.checkPreUploadConditions(layer)) {
+                    model.setUploadState(layer, UploadOrSaveState.FAILED);
                     continue;
                 }
-                final UploadDialog dialog = UploadDialog.getUploadDialog();
-                dialog.setUploadedPrimitives(new APIDataSet(layerInfo.getLayer().data));
-                dialog.setVisible(true);
-                if (dialog.isCanceled()) {
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
+
+                AbstractUploadDialog dialog = layer.getUploadDialog();
+                if (dialog != null) {
+                    dialog.setVisible(true);
+                    if (dialog.isCanceled()) {
+                        model.setUploadState(layer, UploadOrSaveState.CANCELED);
+                        continue;
+                    }
+                    dialog.rememberUserInput();
+                }
+
+                currentTask = layer.createUploadTask(monitor);
+                if (currentTask == null) {
+                    model.setUploadState(layer, UploadOrSaveState.FAILED);
                     continue;
                 }
-                dialog.rememberUserInput();
-
-                currentTask = new UploadLayerTask(
-                        UploadDialog.getUploadDialog().getUploadStrategySpecification(),
-                        layerInfo.getLayer(),
-                        monitor,
-                        UploadDialog.getUploadDialog().getChangeset()
-                );
                 currentFuture = worker.submit(currentTask);
                 try {
@@ -480,18 +482,18 @@
                     currentFuture.get();
                 } catch(CancellationException e) {
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
+                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
                 } catch(Exception e) {
                     Main.error(e);
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                    model.setUploadState(layer, UploadOrSaveState.FAILED);
                     ExceptionDialogUtil.explainException(e);
                 }
                 if (currentTask.isCanceled()) {
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
+                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
                 } else if (currentTask.isFailed()) {
                     Main.error(currentTask.getLastException());
                     ExceptionDialogUtil.explainException(currentTask.getLastException());
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                    model.setUploadState(layer, UploadOrSaveState.FAILED);
                 } else {
-                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.OK);
+                    model.setUploadState(layer, UploadOrSaveState.OK);
                 }
                 currentTask = null;
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayersModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayersModel.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayersModel.java	(revision 7358)
@@ -12,4 +12,5 @@
 import javax.swing.table.DefaultTableModel;
 
+import org.openstreetmap.josm.gui.layer.ModifiableLayer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 
@@ -59,8 +60,13 @@
     }
 
-    public void populate(List<OsmDataLayer> layers) {
+    /**
+     * Populates the model with given modifiable layers.
+     * @param layers The layers to use to populate this model
+     * @since 7358
+     */
+    public void populate(List<? extends ModifiableLayer> layers) {
         layerInfo = new ArrayList<>();
         if (layers == null) return;
-        for (OsmDataLayer layer: layers) {
+        for (ModifiableLayer layer: layers) {
             layerInfo.add(new SaveLayerInfo(layer));
         }
@@ -135,7 +141,8 @@
 
     public List<SaveLayerInfo> getLayersWithConflictsAndUploadRequest() {
-        List<SaveLayerInfo> ret =new ArrayList<>();
-        for (SaveLayerInfo info: layerInfo) {
-            if (info.isDoUploadToServer() && !info.getLayer().getConflicts().isEmpty()) {
+        List<SaveLayerInfo> ret = new ArrayList<>();
+        for (SaveLayerInfo info: layerInfo) {
+            ModifiableLayer l = info.getLayer();
+            if (info.isDoUploadToServer() && l instanceof OsmDataLayer && !((OsmDataLayer)l).getConflicts().isEmpty()) {
                 ret.add(info);
             }
@@ -145,5 +152,5 @@
 
     public List<SaveLayerInfo> getLayersToUpload() {
-        List<SaveLayerInfo> ret =new ArrayList<>();
+        List<SaveLayerInfo> ret = new ArrayList<>();
         for (SaveLayerInfo info: layerInfo) {
             if (info.isDoUploadToServer()) {
@@ -155,5 +162,5 @@
 
     public List<SaveLayerInfo> getLayersToSave() {
-        List<SaveLayerInfo> ret =new ArrayList<>();
+        List<SaveLayerInfo> ret = new ArrayList<>();
         for (SaveLayerInfo info: layerInfo) {
             if (info.isDoSaveToFile()) {
@@ -164,5 +171,5 @@
     }
 
-    public void setUploadState(OsmDataLayer layer, UploadOrSaveState state) {
+    public void setUploadState(ModifiableLayer layer, UploadOrSaveState state) {
         SaveLayerInfo info = getSaveLayerInfo(layer);
         if (info != null) {
@@ -172,5 +179,5 @@
     }
 
-    public void setSaveState(OsmDataLayer layer, UploadOrSaveState state) {
+    public void setSaveState(ModifiableLayer layer, UploadOrSaveState state) {
         SaveLayerInfo info = getSaveLayerInfo(layer);
         if (info != null) {
@@ -180,5 +187,5 @@
     }
 
-    public SaveLayerInfo getSaveLayerInfo(OsmDataLayer layer) {
+    public SaveLayerInfo getSaveLayerInfo(ModifiableLayer layer) {
         for (SaveLayerInfo info: this.layerInfo) {
             if (info.getLayer() == layer)
Index: trunk/src/org/openstreetmap/josm/gui/io/UploadDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/UploadDialog.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/io/UploadDialog.java	(revision 7358)
@@ -31,5 +31,4 @@
 import javax.swing.JButton;
 import javax.swing.JComponent;
-import javax.swing.JDialog;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
@@ -59,7 +58,7 @@
  * This is a dialog for entering upload options like the parameters for
  * the upload changeset and the strategy for opening/closing a changeset.
- *
+ * @since 2025
  */
-public class UploadDialog extends JDialog implements PropertyChangeListener, PreferenceChangedListener{
+public class UploadDialog extends AbstractUploadDialog implements PropertyChangeListener, PreferenceChangedListener {
     /**  the unique instance of the upload dialog */
     private static UploadDialog uploadDialog;
@@ -97,5 +96,4 @@
     /** the upload button */
     private JButton btnUpload;
-    private boolean canceled = false;
 
     /** the changeset comment model keeping the state of the changeset comment */
@@ -267,7 +265,5 @@
     }
 
-    /**
-     * Remembers the user input in the preference settings
-     */
+    @Override
     public void rememberUserInput() {
         pnlBasicUploadSettings.rememberUserInput();
@@ -345,22 +341,4 @@
     protected String getUploadSource() {
         return changesetSourceModel.getComment();
-    }
-
-    /**
-     * Returns true if the dialog was canceled
-     *
-     * @return true if the dialog was canceled
-     */
-    public boolean isCanceled() {
-        return canceled;
-    }
-
-    /**
-     * Sets whether the dialog was canceled
-     *
-     * @param canceled true if the dialog is canceled
-     */
-    protected void setCanceled(boolean canceled) {
-        this.canceled = canceled;
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/io/UploadLayerTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/UploadLayerTask.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/io/UploadLayerTask.java	(revision 7358)
@@ -40,5 +40,5 @@
  * </pre>
  */
-class UploadLayerTask extends AbstractIOTask implements Runnable {
+public class UploadLayerTask extends AbstractIOTask implements Runnable {
     private OsmServerWriter writer;
     private OsmDataLayer layer;
Index: trunk/src/org/openstreetmap/josm/gui/layer/Layer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/Layer.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/layer/Layer.java	(revision 7358)
@@ -119,4 +119,5 @@
     /**
      * Create the layer and fill in the necessary components.
+     * @param name Layer name
      */
     public Layer(String name) {
Index: trunk/src/org/openstreetmap/josm/gui/layer/ModifiableLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/ModifiableLayer.java	(revision 7358)
+++ trunk/src/org/openstreetmap/josm/gui/layer/ModifiableLayer.java	(revision 7358)
@@ -0,0 +1,96 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer;
+
+import org.openstreetmap.josm.gui.io.AbstractIOTask;
+import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * A modifiable layer.
+ * @since 7358
+ */
+public abstract class ModifiableLayer extends Layer {
+
+    /**
+     * Constructs a new {@code ModifiableLayer}.
+     * @param name Layer name
+     */
+    public ModifiableLayer(String name) {
+        super(name);
+    }
+
+    /**
+     * Determines if the data managed by this layer needs to be uploaded to
+     * the server because it contains modified data.
+     *
+     * @return true if the data managed by this layer needs to be uploaded to
+     * the server because it contains modified data; false, otherwise
+     */
+    public boolean requiresUploadToServer() {
+        // Override if needed
+        return false;
+    }
+
+    /**
+     * Determines if the data managed by this layer needs to be saved to
+     * a file. Only replies true if a file is assigned to this layer and
+     * if the data managed by this layer has been modified since the last
+     * save operation to the file.
+     *
+     * @return true if the data managed by this layer needs to be saved to a file
+     */
+    public boolean requiresSaveToFile() {
+        // Override if needed
+        return false;
+    }
+
+    /**
+     * Determines if upload of data managed by this layer is discouraged.
+     * This feature allows to use "private" data layers.
+     *
+     * @return true if upload is discouraged for this layer; false, otherwise
+     */
+    public boolean isUploadDiscouraged() {
+        // Override if needed
+        return false;
+    }
+
+    /**
+     * Determines if data managed by this layer has been modified.
+     * @return true if data has been modified; false, otherwise
+     */
+    public abstract boolean isModified();
+
+    /**
+     * Initializes the layer after a successful save of data to a file.
+     */
+    public void onPostSaveToFile() {
+        // Override if needed
+    }
+
+    /**
+     * Initializes the layer after a successful upload to the server.
+     */
+    public void onPostUploadToServer() {
+        // Override if needed
+    }
+
+    /**
+     * Creates a new {@code AbstractIOTask} for uploading data.
+     * @param monitor The progress monitor
+     * @return a new {@code AbstractIOTask} for uploading data, or {@code null} if not applicable
+     */
+    public AbstractIOTask createUploadTask(ProgressMonitor monitor) {
+        // Override if needed
+        return null;
+    }
+
+    /**
+     * Returns the upload dialog for this layer.
+     * @return the upload dialog for this layer, or {@code null} if not applicable
+     */
+    public AbstractUploadDialog getUploadDialog() {
+        // Override if needed
+        return null;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 7356)
+++ trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 7358)
@@ -44,4 +44,5 @@
 import org.openstreetmap.josm.actions.SaveActionBase;
 import org.openstreetmap.josm.actions.ToggleUploadDiscouragedLayerAction;
+import org.openstreetmap.josm.data.APIDataSet;
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.SelectionChangedListener;
@@ -76,4 +77,8 @@
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.gui.io.AbstractIOTask;
+import org.openstreetmap.josm.gui.io.AbstractUploadDialog;
+import org.openstreetmap.josm.gui.io.UploadDialog;
+import org.openstreetmap.josm.gui.io.UploadLayerTask;
 import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
@@ -91,5 +96,5 @@
  * @author imi
  */
-public class OsmDataLayer extends Layer implements Listener, SelectionChangedListener {
+public class OsmDataLayer extends ModifiableLayer implements Listener, SelectionChangedListener {
     public static final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
     public static final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
@@ -622,25 +627,10 @@
     }
 
-    /**
-     * Replies true if the data managed by this layer needs to be uploaded to
-     * the server because it contains at least one modified primitive.
-     *
-     * @return true if the data managed by this layer needs to be uploaded to
-     * the server because it contains at least one modified primitive; false,
-     * otherwise
-     */
+    @Override
     public boolean requiresUploadToServer() {
         return requiresUploadToServer;
     }
 
-    /**
-     * Replies true if the data managed by this layer needs to be saved to
-     * a file. Only replies true if a file is assigned to this layer and
-     * if the data managed by this layer has been modified since the last
-     * save operation to the file.
-     *
-     * @return true if the data managed by this layer needs to be saved to
-     * a file
-     */
+    @Override
     public boolean requiresSaveToFile() {
         return getAssociatedFile() != null && requiresSaveToFile;
@@ -650,10 +640,10 @@
     public void onPostLoadFromFile() {
         setRequiresSaveToFile(false);
-        setRequiresUploadToServer(data.isModified());
+        setRequiresUploadToServer(isModified());
     }
 
     public void onPostDownloadFromServer() {
         setRequiresSaveToFile(true);
-        setRequiresUploadToServer(data.isModified());
+        setRequiresUploadToServer(isModified());
     }
 
@@ -663,19 +653,13 @@
     }
 
-    /**
-     * Initializes the layer after a successful save of OSM data to a file
-     *
-     */
+    @Override
     public void onPostSaveToFile() {
         setRequiresSaveToFile(false);
-        setRequiresUploadToServer(data.isModified());
-    }
-
-    /**
-     * Initializes the layer after a successful upload to the server
-     *
-     */
+        setRequiresUploadToServer(isModified());
+    }
+
+    @Override
     public void onPostUploadToServer() {
-        setRequiresUploadToServer(data.isModified());
+        setRequiresUploadToServer(isModified());
         // keep requiresSaveToDisk unchanged
     }
@@ -730,4 +714,5 @@
     }
 
+    @Override
     public final boolean isUploadDiscouraged() {
         return data.isUploadDiscouraged();
@@ -741,4 +726,9 @@
             }
         }
+    }
+
+    @Override
+    public final boolean isModified() {
+        return data.isModified();
     }
 
@@ -810,3 +800,20 @@
         return SaveActionBase.createAndOpenSaveFileChooser(tr("Save OSM file"), "osm");
     }
+
+    @Override
+    public AbstractIOTask createUploadTask(final ProgressMonitor monitor) {
+        UploadDialog dialog = UploadDialog.getUploadDialog();
+        return new UploadLayerTask(
+                dialog.getUploadStrategySpecification(),
+                this,
+                monitor,
+                dialog.getChangeset());
+    }
+
+    @Override
+    public AbstractUploadDialog getUploadDialog() {
+        UploadDialog dialog = UploadDialog.getUploadDialog();
+        dialog.setUploadedPrimitives(new APIDataSet(data));
+        return dialog;
+    }
 }
