Index: src/org/openstreetmap/josm/gui/download/IDownloadSourceType.java
===================================================================
--- src/org/openstreetmap/josm/gui/download/IDownloadSourceType.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/download/IDownloadSourceType.java	(working copy)
@@ -0,0 +1,57 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.download;
+
+import javax.swing.JCheckBox;
+import javax.swing.event.ChangeListener;
+
+import org.openstreetmap.josm.actions.downloadtasks.AbstractDownloadTask;
+import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+
+/**
+ * An interface to allow arbitrary download sources and types in the primary
+ * download window of JOSM
+ *
+ * @since xxx
+ */
+public interface IDownloadSourceType {
+    /**
+     * @return The checkbox to be added to the UI
+     */
+    default JCheckBox getCheckBox() {
+        return getCheckBox(null);
+    }
+
+    /**
+     * @param checkboxChangeListener The listener for checkboxes (may be
+     *                               {@code null})
+     * @return The checkbox to be added to the UI
+     */
+    JCheckBox getCheckBox(ChangeListener checkboxChangeListener);
+
+    /**
+     * @return The {@link DownloadTask} class which will be getting the data
+     */
+    Class<? extends AbstractDownloadTask<?>> getDownloadClass();
+
+    /**
+     * @return The boolean indicating the last state of the download type
+     */
+    default boolean isEnabled() {
+        return getBooleanProperty().get();
+    }
+
+    /**
+     * @return The boolean property for this particular download type
+     */
+    BooleanProperty getBooleanProperty();
+
+    /**
+     * Check if the area is too large for the current IDownloadSourceType
+     *
+     * @param bound The bound that will be downloaded
+     * @return {@code true} if we definitely cannot download the area;
+     */
+    boolean isDownloadAreaTooLarge(Bounds bound);
+}
Index: src/org/openstreetmap/josm/gui/download/OSMDownloadSource.java
===================================================================
--- src/org/openstreetmap/josm/gui/download/OSMDownloadSource.java	(revision 16500)
+++ src/org/openstreetmap/josm/gui/download/OSMDownloadSource.java	(working copy)
@@ -8,7 +8,9 @@
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.GridBagLayout;
+import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
@@ -17,6 +19,7 @@
 import javax.swing.JCheckBox;
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
+import javax.swing.JPanel;
 import javax.swing.event.ChangeListener;
 
 import org.openstreetmap.josm.actions.downloadtasks.AbstractDownloadTask;
@@ -28,6 +31,9 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.data.ViewportData;
+import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.NoteData;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MapFrame;
@@ -42,7 +48,7 @@
  * Class defines the way data is fetched from the OSM server.
  * @since 12652
  */
-public class OSMDownloadSource implements DownloadSource<OSMDownloadSource.OSMDownloadData> {
+public class OSMDownloadSource implements DownloadSource<List<IDownloadSourceType>> {
     /**
      * The simple name for the {@link OSMDownloadSourcePanel}
      * @since 12706
@@ -49,49 +55,43 @@
      */
     public static final String SIMPLE_NAME = "osmdownloadpanel";
 
+    /** The possible methods to get data */
+    static final List<IDownloadSourceType> DOWNLOAD_SOURCES = new ArrayList<>();
+    static {
+        // Order is important (determines button order, and what gets zoomed to)
+        DOWNLOAD_SOURCES.add(new OsmDataDownloadType());
+        DOWNLOAD_SOURCES.add(new GpsDataDownloadType());
+        DOWNLOAD_SOURCES.add(new NotesDataDownloadType());
+    }
+
     @Override
-    public AbstractDownloadSourcePanel<OSMDownloadData> createPanel(DownloadDialog dialog) {
+    public AbstractDownloadSourcePanel<List<IDownloadSourceType>> createPanel(DownloadDialog dialog) {
         return new OSMDownloadSourcePanel(this, dialog);
     }
 
     @Override
-    public void doDownload(OSMDownloadData data, DownloadSettings settings) {
+    public void doDownload(List<IDownloadSourceType> data, DownloadSettings settings) {
         Bounds bbox = settings.getDownloadBounds()
                 .orElseThrow(() -> new IllegalArgumentException("OSM downloads requires bounds"));
         boolean zoom = settings.zoomToData();
         boolean newLayer = settings.asNewLayer();
-        List<Pair<AbstractDownloadTask<?>, Future<?>>> tasks = new ArrayList<>();
-
-        if (data.isDownloadOSMData()) {
-            DownloadOsmTask task = new DownloadOsmTask();
-            task.setZoomAfterDownload(zoom && !data.isDownloadGPX() && !data.isDownloadNotes());
-            Future<?> future = task.download(new DownloadParams().withNewLayer(newLayer), bbox, null);
-            MainApplication.worker.submit(new PostDownloadHandler(task, future));
-            if (zoom) {
-                tasks.add(new Pair<>(task, future));
+        final List<Pair<AbstractDownloadTask<?>, Future<?>>> tasks = new ArrayList<>();
+        IDownloadSourceType zoomTask = zoom ? data.stream().findFirst().orElse(null) : null;
+        data.stream().filter(IDownloadSourceType::isEnabled).forEach(type -> {
+            try {
+                AbstractDownloadTask<?> task = type.getDownloadClass().getDeclaredConstructor().newInstance();
+                task.setZoomAfterDownload(type.equals(zoomTask));
+                Future<?> future = task.download(new DownloadParams().withNewLayer(newLayer), bbox, null);
+                MainApplication.worker.submit(new PostDownloadHandler(task, future));
+                if (zoom) {
+                    tasks.add(new Pair<AbstractDownloadTask<?>, Future<?>>(task, future));
+                }
+            } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
+                    | InvocationTargetException | NoSuchMethodException | SecurityException e) {
+                Logging.error(e);
             }
-        }
+        });
 
-        if (data.isDownloadGPX()) {
-            DownloadGpsTask task = new DownloadGpsTask();
-            task.setZoomAfterDownload(zoom && !data.isDownloadOSMData() && !data.isDownloadNotes());
-            Future<?> future = task.download(new DownloadParams().withNewLayer(newLayer), bbox, null);
-            MainApplication.worker.submit(new PostDownloadHandler(task, future));
-            if (zoom) {
-                tasks.add(new Pair<>(task, future));
-            }
-        }
-
-        if (data.isDownloadNotes()) {
-            DownloadNotesTask task = new DownloadNotesTask();
-            task.setZoomAfterDownload(zoom && !data.isDownloadOSMData() && !data.isDownloadGPX());
-            Future<?> future = task.download(new DownloadParams(), bbox, null);
-            MainApplication.worker.submit(new PostDownloadHandler(task, future));
-            if (zoom) {
-                tasks.add(new Pair<>(task, future));
-            }
-        }
-
         if (zoom && tasks.size() > 1) {
             MainApplication.worker.submit(() -> {
                 ProjectionBounds bounds = null;
@@ -130,20 +130,74 @@
     }
 
     /**
+     * @return The possible downloads that JOSM can make in the default Download
+     *         screen
+     * @since xxx
+     */
+    public static List<IDownloadSourceType> getDownloadTypes() {
+        return Collections.unmodifiableList(DOWNLOAD_SOURCES);
+    }
+
+    /**
+     * Get the instance of a data download type
+     *
+     * @param <T> The type to get
+     * @param typeClazz The class of the type
+     * @return The type instance
+     * @since xxx
+     */
+    public static <T extends IDownloadSourceType> T getDownloadType(Class<T> typeClazz) {
+        return DOWNLOAD_SOURCES.stream().filter(typeClazz::isInstance).map(typeClazz::cast).findFirst().orElse(null);
+    }
+
+    /**
+     * @param type The IDownloadSourceType object to remove
+     * @return true See {@link List#remove}, but it also returns false if the
+     * parameter is a class from JOSM core.
+     * @since xxx
+     */
+    public static boolean removeDownloadType(IDownloadSourceType type) {
+        boolean modified = false;
+        if (!(type instanceof OsmDataDownloadType) && !(type instanceof GpsDataDownloadType)
+                && !(type instanceof NotesDataDownloadType)) {
+            modified = DOWNLOAD_SOURCES.remove(type);
+        }
+        return modified;
+    }
+
+    /**
+     * Add a download type to the default JOSM download window
+     *
+     * @param type The initialized type to download
+     * @return See {@link List#add}, but it also returns false if the class
+     * already has an instance in the list or it is a class from JOSM core.
+     * @since xxx
+     */
+    public static boolean addDownloadType(IDownloadSourceType type) {
+        boolean modified = false;
+        if (!(type instanceof OsmDataDownloadType) && !(type instanceof GpsDataDownloadType)
+                && !(type instanceof NotesDataDownloadType)
+                || DOWNLOAD_SOURCES.stream()
+                        .noneMatch(possibility -> type.getClass().isInstance(possibility))) {
+            modified = DOWNLOAD_SOURCES.add(type);
+        } else {
+            throw new IllegalArgumentException("There can only be one instance of a class added, and it cannot be a built-in class.");
+        }
+        return modified;
+    }
+
+    /**
      * The GUI representation of the OSM download source.
      * @since 12652
      */
-    public static class OSMDownloadSourcePanel extends AbstractDownloadSourcePanel<OSMDownloadData> {
-
-        private final JCheckBox cbDownloadOsmData;
-        private final JCheckBox cbDownloadGpxData;
-        private final JCheckBox cbDownloadNotes;
+    public static class OSMDownloadSourcePanel extends AbstractDownloadSourcePanel<List<IDownloadSourceType>> {
         private final JLabel sizeCheck = new JLabel();
 
-        private static final BooleanProperty DOWNLOAD_OSM = new BooleanProperty("download.osm.data", true);
-        private static final BooleanProperty DOWNLOAD_GPS = new BooleanProperty("download.osm.gps", false);
-        private static final BooleanProperty DOWNLOAD_NOTES = new BooleanProperty("download.osm.notes", false);
+        /** This is used to keep track of the components for download sources, and to dynamically update/remove them */
+        private JPanel downloadSourcesPanel;
 
+        private ChangeListener checkboxChangeListener;
+
         /**
          * Label used in front of data types available for download. Made public for reuse in other download dialogs.
          * @since 16155
@@ -161,57 +215,54 @@
             setLayout(new GridBagLayout());
 
             // size check depends on selected data source
-            final ChangeListener checkboxChangeListener = e ->
+            checkboxChangeListener = e ->
                     dialog.getSelectedDownloadArea().ifPresent(this::updateSizeCheck);
 
             // adding the download tasks
             add(new JLabel(tr(DATA_SOURCES_AND_TYPES)), GBC.std().insets(5, 5, 1, 5).anchor(GBC.CENTER));
-            cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true);
-            cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area."));
-            cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
-
-            cbDownloadGpxData = new JCheckBox(tr("Raw GPS data"));
-            cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area."));
-            cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener);
-
-            cbDownloadNotes = new JCheckBox(tr("Notes"));
-            cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area."));
-            cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener);
-
             Font labelFont = sizeCheck.getFont();
             sizeCheck.setFont(labelFont.deriveFont(Font.PLAIN, labelFont.getSize()));
 
-            add(cbDownloadOsmData, GBC.std().insets(1, 5, 1, 5));
-            add(cbDownloadGpxData, GBC.std().insets(1, 5, 1, 5));
-            add(cbDownloadNotes, GBC.eol().insets(1, 5, 1, 5));
+            downloadSourcesPanel = new JPanel();
+            add(downloadSourcesPanel, GBC.eol().anchor(GBC.EAST));
+            updateSources();
             add(sizeCheck, GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2));
 
             setMinimumSize(new Dimension(450, 115));
         }
 
+        /**
+         * Update the source list for downloading data
+         */
+        protected void updateSources() {
+            downloadSourcesPanel.removeAll();
+            DOWNLOAD_SOURCES
+                .forEach(obj -> downloadSourcesPanel.add(obj.getCheckBox(checkboxChangeListener), GBC.std().insets(1, 5, 1, 5)));
+        }
+
         @Override
-        public OSMDownloadData getData() {
-            return new OSMDownloadData(
-                    isDownloadOsmData(),
-                    isDownloadNotes(),
-                    isDownloadGpxData());
+        public List<IDownloadSourceType> getData() {
+            return DOWNLOAD_SOURCES;
         }
 
         @Override
         public void rememberSettings() {
-            DOWNLOAD_OSM.put(isDownloadOsmData());
-            DOWNLOAD_GPS.put(isDownloadGpxData());
-            DOWNLOAD_NOTES.put(isDownloadNotes());
+            DOWNLOAD_SOURCES.forEach(type -> type.getBooleanProperty().put(type.getCheckBox().isSelected()));
         }
 
         @Override
         public void restoreSettings() {
-            cbDownloadOsmData.setSelected(DOWNLOAD_OSM.get());
-            cbDownloadGpxData.setSelected(DOWNLOAD_GPS.get());
-            cbDownloadNotes.setSelected(DOWNLOAD_NOTES.get());
+            updateSources();
+            DOWNLOAD_SOURCES.forEach(type -> type.getCheckBox().setSelected(type.isEnabled()));
         }
 
         @Override
+        public void setVisible(boolean aFlag) {
+            super.setVisible(aFlag);
+            updateSources();
+        }
+
+        @Override
         public boolean checkDownload(DownloadSettings settings) {
             /*
              * It is mandatory to specify the area to download from OSM.
@@ -232,15 +283,19 @@
              * must be chosen : raw osm data, gpx data, notes.
              * If none of those are selected, then the corresponding dialog is shown to inform the user.
              */
-            if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) {
+            if (DOWNLOAD_SOURCES.stream().noneMatch(IDownloadSourceType::isEnabled)) {
+                StringBuilder line1 = new StringBuilder("<html>").append(tr("None of"));
+                StringBuilder line2 = new StringBuilder(tr("Please choose to either download"));
+
+                DOWNLOAD_SOURCES.forEach(type -> {
+                    line1.append(" <strong>").append(type.getCheckBox().getText()).append("</strong> ");
+                    line2.append(' ').append(type.getCheckBox().getText()).append(tr(", or"));
+                });
+                line1.append(tr("is enabled.")).append("<br>");
+                line2.append(tr(" all.")).append("</html>");
                 JOptionPane.showMessageDialog(
                         this.getParent(),
-                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>"
-                                        + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
-                                cbDownloadOsmData.getText(),
-                                cbDownloadGpxData.getText(),
-                                cbDownloadNotes.getText()
-                        ),
+                        line1.append(line2).toString(),
                         tr("Error"),
                         JOptionPane.ERROR_MESSAGE
                 );
@@ -257,9 +312,12 @@
          * Replies true if the user selected to download OSM data
          *
          * @return true if the user selected to download OSM data
+         * @deprecated since xxx -- use {@link OSMDownloadSource#getDownloadTypes} with
+         *             {@code get(0).getCheckBox().isSelected()}
          */
+        @Deprecated
         public boolean isDownloadOsmData() {
-            return cbDownloadOsmData.isSelected();
+            return DOWNLOAD_SOURCES.get(0).getCheckBox().isSelected();
         }
 
         /**
@@ -266,9 +324,12 @@
          * Replies true if the user selected to download GPX data
          *
          * @return true if the user selected to download GPX data
+         * @deprecated since xxx -- use {@link OSMDownloadSource#getDownloadTypes} with
+         *             {@code get(1).getCheckBox().isSelected()}
          */
+        @Deprecated
         public boolean isDownloadGpxData() {
-            return cbDownloadGpxData.isSelected();
+            return DOWNLOAD_SOURCES.get(1).getCheckBox().isSelected();
         }
 
         /**
@@ -275,9 +336,12 @@
          * Replies true if user selected to download notes
          *
          * @return true if user selected to download notes
+         * @deprecated since xxx -- use {@link OSMDownloadSource#getDownloadTypes} with
+         *             {@code get(2).getCheckBox().isSelected()}
          */
+        @Deprecated
         public boolean isDownloadNotes() {
-            return cbDownloadNotes.isSelected();
+            return DOWNLOAD_SOURCES.get(2).getCheckBox().isSelected();
         }
 
         @Override
@@ -302,18 +366,8 @@
                 return;
             }
 
-            boolean isAreaTooLarge = false;
-            if (!isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) {
-                isAreaTooLarge = false;
-            } else if (isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) {
-                // see max_note_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/settings.yml
-                isAreaTooLarge = bbox.getArea() > Config.getPref().getDouble("osm-server.max-request-area-notes", 25);
-            } else {
-                // see max_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/settings.yml
-                isAreaTooLarge = bbox.getArea() > Config.getPref().getDouble("osm-server.max-request-area", 0.25);
-            }
-
-            displaySizeCheckResult(isAreaTooLarge);
+            displaySizeCheckResult(DOWNLOAD_SOURCES.stream()
+                    .anyMatch(type -> type.isDownloadAreaTooLarge(bbox)));
         }
 
         private void displaySizeCheckResult(boolean isAreaTooLarge) {
@@ -332,26 +386,125 @@
      * Encapsulates data that is required to download from the OSM server.
      */
     static class OSMDownloadData {
-        private final boolean downloadOSMData;
-        private final boolean downloadNotes;
-        private final boolean downloadGPX;
 
-        OSMDownloadData(boolean downloadOSMData, boolean downloadNotes, boolean downloadGPX) {
-            this.downloadOSMData = downloadOSMData;
-            this.downloadNotes = downloadNotes;
-            this.downloadGPX = downloadGPX;
+        private List<IDownloadSourceType> downloadPossibilities;
+
+        /**
+         * @param downloadPossibilities A list of DataDownloadTypes (instantiated, with
+         *                              options set)
+         */
+        OSMDownloadData(List<IDownloadSourceType> downloadPossibilities) {
+            this.downloadPossibilities = downloadPossibilities;
         }
 
-        boolean isDownloadOSMData() {
-            return downloadOSMData;
+        /**
+         * @return A list of DataDownloadTypes (instantiated, with options set)
+         */
+        public List<IDownloadSourceType> getDownloadPossibilities() {
+            return downloadPossibilities;
         }
+    }
 
-        boolean isDownloadNotes() {
-            return downloadNotes;
+    private static class OsmDataDownloadType implements IDownloadSourceType {
+        static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.data", true);
+        JCheckBox cbDownloadOsmData;
+
+        @Override
+        public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) {
+            if (cbDownloadOsmData == null) {
+                cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true);
+                cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area."));
+                cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
+            }
+            if (checkboxChangeListener != null) {
+                cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
+            }
+            return cbDownloadOsmData;
         }
 
-        boolean isDownloadGPX() {
-            return downloadGPX;
+        @Override
+        public Class<? extends AbstractDownloadTask<DataSet>> getDownloadClass() {
+            return DownloadOsmTask.class;
         }
+
+        @Override
+        public BooleanProperty getBooleanProperty() {
+            return IS_ENABLED;
+        }
+
+        @Override
+        public boolean isDownloadAreaTooLarge(Bounds bound) {
+            // see max_request_area in
+            // https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
+            return bound.getArea() > Config.getPref().getDouble("osm-server.max-request-area", 0.25);
+        }
     }
+
+    private static class GpsDataDownloadType implements IDownloadSourceType {
+        static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.gps", false);
+        private JCheckBox cbDownloadGpxData;
+
+        @Override
+        public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) {
+            if (cbDownloadGpxData == null) {
+                cbDownloadGpxData = new JCheckBox(tr("Raw GPS data"));
+                cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area."));
+            }
+            if (checkboxChangeListener != null) {
+                cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener);
+            }
+
+            return cbDownloadGpxData;
+        }
+
+        @Override
+        public Class<? extends AbstractDownloadTask<GpxData>> getDownloadClass() {
+            return DownloadGpsTask.class;
+        }
+
+        @Override
+        public BooleanProperty getBooleanProperty() {
+            return IS_ENABLED;
+        }
+
+        @Override
+        public boolean isDownloadAreaTooLarge(Bounds bound) {
+            return false;
+        }
+    }
+
+    private static class NotesDataDownloadType implements IDownloadSourceType {
+        static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.notes", false);
+        private JCheckBox cbDownloadNotes;
+
+        @Override
+        public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) {
+            if (cbDownloadNotes == null) {
+                cbDownloadNotes = new JCheckBox(tr("Notes"));
+                cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area."));
+            }
+            if (checkboxChangeListener != null) {
+                cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener);
+            }
+
+            return cbDownloadNotes;
+        }
+
+        @Override
+        public Class<? extends AbstractDownloadTask<NoteData>> getDownloadClass() {
+            return DownloadNotesTask.class;
+        }
+
+        @Override
+        public BooleanProperty getBooleanProperty() {
+            return IS_ENABLED;
+        }
+
+        @Override
+        public boolean isDownloadAreaTooLarge(Bounds bound) {
+            // see max_note_request_area in
+            // https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
+            return bound.getArea() > Config.getPref().getDouble("osm-server.max-request-area-notes", 25);
+        }
+    }
 }
