Index: src/org/openstreetmap/josm/gui/download/OSMDownloadSource.java
===================================================================
--- src/org/openstreetmap/josm/gui/download/OSMDownloadSource.java	(revision 15528)
+++ src/org/openstreetmap/josm/gui/download/OSMDownloadSource.java	(working copy)
@@ -7,7 +7,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;
@@ -23,10 +25,14 @@
 import org.openstreetmap.josm.actions.downloadtasks.DownloadNotesTask;
 import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
 import org.openstreetmap.josm.actions.downloadtasks.DownloadParams;
+import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
 import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
 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;
@@ -48,6 +54,15 @@
      */
     public static final String SIMPLE_NAME = "osmdownloadpanel";
 
+    /** The possible methods to get data */
+    static final List<DataDownloadType> DOWNLOAD_POSSIBILITIES = new ArrayList<>();
+    static {
+        // Order is important (determines button order, and what gets zoomed to)
+        DOWNLOAD_POSSIBILITIES.add(new OsmDataDownloadType());
+        DOWNLOAD_POSSIBILITIES.add(new GpsDataDownloadType());
+        DOWNLOAD_POSSIBILITIES.add(new NotesDataDownloadType());
+    }
+
     @Override
     public AbstractDownloadSourcePanel<OSMDownloadData> createPanel(DownloadDialog dialog) {
         return new OSMDownloadSourcePanel(this, dialog);
@@ -59,38 +74,23 @@
                 .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<>();
+        DataDownloadType zoomTask = zoom ? data.getDownloadPossibilities().stream().findFirst().orElse(null) : null;
+        data.getDownloadPossibilities().parallelStream().filter(DataDownloadType::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;
@@ -129,20 +129,53 @@
     }
 
     /**
+     * @return The possible downloads that JOSM can make in the default Download
+     *         screen
+     * @since xxx
+     */
+    public List<DataDownloadType> getDownloadTypes() {
+        return Collections.unmodifiableList(DOWNLOAD_POSSIBILITIES);
+    }
+
+    /**
+     * @param type The DataDownloadType object to remove
+     * @return true if the list was modified
+     * @since xxx
+     */
+    public boolean removeDownloadType(DataDownloadType type) {
+        boolean modified = false;
+        if (!(type instanceof OsmDataDownloadType) && !(type instanceof GpsDataDownloadType)
+                && !(type instanceof NotesDataDownloadType)) {
+            modified = DOWNLOAD_POSSIBILITIES.remove(type);
+        }
+        return modified;
+    }
+
+    /**
+     * Add a download type to the default JOSM download window
+     *
+     * @param type The initialized type to download
+     * @return true if the list was modified
+     * @since xxx
+     */
+    public boolean addDownloadType(DataDownloadType type) {
+        boolean modified = false;
+        if (!(type instanceof OsmDataDownloadType) && !(type instanceof GpsDataDownloadType)
+                && !(type instanceof NotesDataDownloadType)
+                || DOWNLOAD_POSSIBILITIES.parallelStream()
+                        .noneMatch(possibility -> type.getClass().isInstance(possibility))) {
+            modified = DOWNLOAD_POSSIBILITIES.add(type);
+        }
+        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;
         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);
-
         /**
          * Creates a new {@link OSMDownloadSourcePanel}.
          * @param dialog the parent download dialog, as {@code DownloadDialog.getInstance()} might not be initialized yet
@@ -159,24 +192,11 @@
 
             // 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));
+            DOWNLOAD_POSSIBILITIES
+                    .forEach(obj -> add(obj.getCheckBox(checkboxChangeListener), GBC.std().insets(1, 5, 1, 5)));
             add(sizeCheck, GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2));
 
             setMinimumSize(new Dimension(450, 115));
@@ -184,24 +204,17 @@
 
         @Override
         public OSMDownloadData getData() {
-            return new OSMDownloadData(
-                    isDownloadOsmData(),
-                    isDownloadNotes(),
-                    isDownloadGpxData());
+            return new OSMDownloadData(DOWNLOAD_POSSIBILITIES);
         }
 
         @Override
         public void rememberSettings() {
-            DOWNLOAD_OSM.put(isDownloadOsmData());
-            DOWNLOAD_GPS.put(isDownloadGpxData());
-            DOWNLOAD_NOTES.put(isDownloadNotes());
+            DOWNLOAD_POSSIBILITIES.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());
+            DOWNLOAD_POSSIBILITIES.forEach(type -> type.getCheckBox().setSelected(type.isEnabled()));
         }
 
         @Override
@@ -225,15 +238,23 @@
              * 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_POSSIBILITIES.parallelStream().noneMatch(type -> type.isEnabled())) {
+                StringBuilder line1 = new StringBuilder("<html>");
+                StringBuilder line2 = new StringBuilder(tr("Please choose to either download"));
+                DOWNLOAD_POSSIBILITIES.forEach(type -> {
+                    if (line1.length() == 6) {
+                        line1.append(tr("Neither"));
+                    } else {
+                        line1.append(tr("nor"));
+                    }
+                    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
                 );
@@ -250,9 +271,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_POSSIBILITIES.get(0).getCheckBox().isSelected();
         }
 
         /**
@@ -259,9 +283,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_POSSIBILITIES.get(1).getCheckBox().isSelected();
         }
 
         /**
@@ -268,9 +295,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_POSSIBILITIES.get(2).getCheckBox().isSelected();
         }
 
         @Override
@@ -295,18 +325,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/example.application.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/example.application.yml
-                isAreaTooLarge = bbox.getArea() > Config.getPref().getDouble("osm-server.max-request-area", 0.25);
-            }
-
-            displaySizeCheckResult(isAreaTooLarge);
+            displaySizeCheckResult(DOWNLOAD_POSSIBILITIES.parallelStream()
+                    .anyMatch(type -> type.isDownloadAreaTooLarge(bbox)));
         }
 
         private void displaySizeCheckResult(boolean isAreaTooLarge) {
@@ -325,26 +345,173 @@
      * 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<DataDownloadType> downloadPossibilities;
+
+        /**
+         * @param downloadPossibilities A list of DataDownloadTypes (instantiated, with
+         *                              options set)
+         */
+        OSMDownloadData(List<DataDownloadType> downloadPossibilities) {
+            this.downloadPossibilities = downloadPossibilities;
         }
 
-        boolean isDownloadOSMData() {
-            return downloadOSMData;
+        /**
+         * @return A list of DataDownloadTypes (instantiated, with options set)
+         */
+        public List<DataDownloadType> getDownloadPossibilities() {
+            return downloadPossibilities;
         }
+    }
 
-        boolean isDownloadNotes() {
-            return downloadNotes;
+    /**
+     * An interface to allow arbitrary download sources and types in the primary
+     * download window of JOSM
+     *
+     * @since xxx
+     */
+    interface DataDownloadType {
+        /**
+         * @return The checkbox to be added to the UI
+         */
+        default JCheckBox getCheckBox() {
+            return getCheckBox(null);
         }
 
-        boolean isDownloadGPX() {
-            return downloadGPX;
+        /**
+         * @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 DataDownloadType
+         *
+         * @param bound The bound that will be downloaded
+         * @return {@code true} if we definitely cannot download the area;
+         */
+        boolean isDownloadAreaTooLarge(Bounds bound);
     }
+
+    private static class OsmDataDownloadType implements DataDownloadType {
+        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;
+        }
+
+        @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 DataDownloadType {
+        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 DataDownloadType {
+        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);
+        }
+    }
+
 }
