Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/AllFormatsImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/AllFormatsImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/AllFormatsImporter.java	(revision 12671)
@@ -0,0 +1,45 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.util.Iterator;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+
+/**
+ * Dummy importer that adds the "All Formats"-Filter when opening files
+ */
+public class AllFormatsImporter extends FileImporter {
+    /**
+     * Constructs a new {@code AllFormatsImporter}.
+     */
+    public AllFormatsImporter() {
+        super(new ExtensionFileFilter(getAllExtensions(), "", tr("All Formats")
+                + " (*.gpx *.osm *.nmea *.jpg ...)"));
+    }
+
+    @Override
+    public boolean acceptFile(File pathname) {
+        return false;
+    }
+
+    /**
+     * Builds list of all supported extensions by the registered FileImporters.
+     * @return String comma separated list of supported file extensions
+     */
+    private static String getAllExtensions() {
+        Iterator<FileImporter> imp = ExtensionFileFilter.getImporters().iterator();
+        StringBuilder ext = new StringBuilder();
+        while (imp.hasNext()) {
+            FileImporter fi = imp.next();
+            if (fi instanceof AllFormatsImporter) {
+                continue;
+            }
+            ext.append(fi.filter.getExtensions()).append(',');
+        }
+        // remove last comma
+        return ext.substring(0, ext.length()-1);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/FileExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/FileExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/FileExporter.java	(revision 12671)
@@ -0,0 +1,96 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
+import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
+
+/**
+ * Abstract base class for file exporters - IO classes that save layers to a file.
+ */
+public abstract class FileExporter implements ActiveLayerChangeListener {
+
+    public final ExtensionFileFilter filter;
+
+    private boolean enabled;
+    private boolean canceled;
+
+    /**
+     * Constructs a new {@code FileExporter}.
+     * @param filter The extension file filter
+     */
+    public FileExporter(ExtensionFileFilter filter) {
+        this.filter = filter;
+        this.enabled = true;
+    }
+
+    /**
+     * Check if this exporter can export a certain layer to a certain file.
+     *
+     * Most exporters support just a single layer type.
+     * @param pathname the target file name (check file extension using the {@link #filter}
+     * @param layer the layer requested for export
+     * @return true, if the exporter can handle the layer and filename is okay
+     */
+    public boolean acceptFile(File pathname, Layer layer) {
+        return filter.acceptName(pathname.getName());
+    }
+
+    /**
+     * Execute the data export. (To be overridden by subclasses.)
+     *
+     * @param file target file
+     * @param layer the layer to export
+     * @throws IOException in case of an IO error
+     */
+    public void exportData(File file, Layer layer) throws IOException {
+        throw new IOException(tr("Could not export ''{0}''.", file.getName()));
+    }
+
+    /**
+     * Returns the enabled state of this {@code FileExporter}. When enabled, it is listed and usable in "File-&gt;Save" dialogs.
+     * @return true if this {@code FileExporter} is enabled
+     * @since 5459
+     */
+    public final boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Sets the enabled state of the {@code FileExporter}. When enabled, it is listed and usable in "File-&gt;Save" dialogs.
+     * @param enabled true to enable this {@code FileExporter}, false to disable it
+     * @since 5459
+     */
+    public final void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    @Override
+    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
+        // To be overriden by subclasses if their enabled state depends of the active layer nature
+    }
+
+    /**
+     * Determines if this exporter has been canceled during export.
+     * @return true if this {@code FileExporter} has been canceled
+     * @since 6815
+     */
+    public final boolean isCanceled() {
+        return canceled;
+    }
+
+    /**
+     * Marks this exporter as canceled.
+     * @param canceled true to mark this exporter as canceled, {@code false} otherwise
+     * @since 6815
+     */
+    public final void setCanceled(boolean canceled) {
+        this.canceled = canceled;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/FileImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/FileImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/FileImporter.java	(revision 12671)
@@ -0,0 +1,189 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.HelpAwareOptionPane;
+import org.openstreetmap.josm.gui.Notification;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.IllegalDataException;
+import org.openstreetmap.josm.io.ImportCancelException;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
+import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
+
+/**
+ * Abstract file importer.
+ * @since 1637
+ * @since 10386 (signature)
+ */
+public abstract class FileImporter implements Comparable<FileImporter> {
+
+    /**
+     * The extension file filter used to accept files.
+     */
+    public final ExtensionFileFilter filter;
+
+    private boolean enabled;
+
+    /**
+     * Constructs a new {@code FileImporter} with the given extension file filter.
+     * @param filter The extension file filter
+     */
+    public FileImporter(ExtensionFileFilter filter) {
+        this.filter = filter;
+        this.enabled = true;
+    }
+
+    /**
+     * Determines if this file importer accepts the given file.
+     * @param pathname The file to test
+     * @return {@code true} if this file importer accepts the given file, {@code false} otherwise
+     */
+    public boolean acceptFile(File pathname) {
+        return filter.acceptName(pathname.getName());
+    }
+
+    /**
+     * A batch importer is a file importer that prefers to read multiple files at the same time.
+     * @return {@code true} if this importer is a batch importer
+     */
+    public boolean isBatchImporter() {
+        return false;
+    }
+
+    /**
+     * Needs to be implemented if isBatchImporter() returns false.
+     * @param file file to import
+     * @param progressMonitor progress monitor
+     * @throws IOException if any I/O error occurs
+     * @throws IllegalDataException if invalid data is read
+     */
+    public void importData(File file, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
+        throw new IOException(tr("Could not import ''{0}''.", file.getName()));
+    }
+
+    /**
+     * Needs to be implemented if isBatchImporter() returns true.
+     * @param files files to import
+     * @param progressMonitor progress monitor
+     * @throws IOException if any I/O error occurs
+     * @throws IllegalDataException if invalid data is read
+     */
+    public void importData(List<File> files, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
+        throw new IOException(tr("Could not import files."));
+    }
+
+    /**
+     * Wrapper to {@link #importData(File, ProgressMonitor)} to give meaningful output if things go wrong.
+     * @param f data file to import
+     * @param progressMonitor progress monitor
+     * @return true if data import was successful
+     */
+    public boolean importDataHandleExceptions(File f, ProgressMonitor progressMonitor) {
+        try {
+            Logging.info("Open file: " + f.getAbsolutePath() + " (" + f.length() + " bytes)");
+            importData(f, progressMonitor);
+            return true;
+        } catch (IllegalDataException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof ImportCancelException) {
+                displayCancel(cause);
+            } else {
+                displayError(f, e);
+            }
+            return false;
+        } catch (IOException e) {
+            displayError(f, e);
+            return false;
+        } catch (RuntimeException | LinkageError e) { // NOPMD
+            BugReportExceptionHandler.handleException(e);
+            return false;
+        }
+    }
+
+    private static void displayError(File f, Exception e) {
+        Logging.error(e);
+        HelpAwareOptionPane.showMessageDialogInEDT(
+                Main.parent,
+                tr("<html>Could not read file ''{0}''.<br>Error is:<br>{1}</html>",
+                        f.getName(), Utils.escapeReservedCharactersHTML(e.getMessage())),
+                tr("Error"),
+                JOptionPane.ERROR_MESSAGE, null
+        );
+    }
+
+    private static void displayCancel(final Throwable t) {
+        GuiHelper.runInEDTAndWait(() -> {
+            Notification note = new Notification(t.getMessage());
+            note.setIcon(JOptionPane.INFORMATION_MESSAGE);
+            note.setDuration(Notification.TIME_SHORT);
+            note.show();
+        });
+    }
+
+    /**
+     * Wrapper to {@link #importData(List, ProgressMonitor)} to give meaningful output if things go wrong.
+     * @param files data files to import
+     * @param progressMonitor progress monitor
+     * @return true if data import was successful
+     */
+    public boolean importDataHandleExceptions(List<File> files, ProgressMonitor progressMonitor) {
+        try {
+            Logging.info("Open "+files.size()+" files");
+            importData(files, progressMonitor);
+            return true;
+        } catch (IOException | IllegalDataException e) {
+            Logging.error(e);
+            HelpAwareOptionPane.showMessageDialogInEDT(
+                    Main.parent,
+                    tr("<html>Could not read files.<br>Error is:<br>{0}</html>", Utils.escapeReservedCharactersHTML(e.getMessage())),
+                    tr("Error"),
+                    JOptionPane.ERROR_MESSAGE, null
+            );
+            return false;
+        }
+    }
+
+    /**
+     * If multiple files (with multiple file formats) are selected,
+     * they are opened in the order of their priorities.
+     * Highest priority comes first.
+     * @return priority
+     */
+    public double getPriority() {
+        return 0;
+    }
+
+    @Override
+    public int compareTo(FileImporter other) {
+        return Double.compare(this.getPriority(), other.getPriority());
+    }
+
+    /**
+     * Returns the enabled state of this {@code FileImporter}. When enabled, it is listed and usable in "File-&gt;Open" dialog.
+     * @return true if this {@code FileImporter} is enabled
+     * @since 5459
+     */
+    public final boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Sets the enabled state of the {@code FileImporter}. When enabled, it is listed and usable in "File-&gt;Open" dialog.
+     * @param enabled true to enable this {@code FileImporter}, false to disable it
+     * @since 5459
+     */
+    public final void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/GeoJSONExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/GeoJSONExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/GeoJSONExporter.java	(revision 12671)
@@ -0,0 +1,44 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.io.GeoJSONWriter;
+
+/**
+ * Exporter to write map data to a GeoJSON file.
+ * @since 4886
+ */
+public class GeoJSONExporter extends FileExporter {
+
+    /** File extension filter for .geojson files */
+    public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter(
+            "geojson,json", "geojson", tr("GeoJSON Files") + " (*.geojson *.json)");
+
+    /**
+     * Constructs a new {@code GeoJSONExporter} with WGS84 projection.
+     */
+    public GeoJSONExporter() {
+        super(FILE_FILTER);
+    }
+
+    @Override
+    public void exportData(File file, Layer layer) throws IOException {
+        if (layer instanceof OsmDataLayer) {
+            try (Writer out = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
+                out.write(new GeoJSONWriter((OsmDataLayer) layer).write());
+            }
+        } else {
+            throw new IllegalArgumentException(tr("Layer ''{0}'' not supported", layer.getClass().toString()));
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxExporter.java	(revision 12671)
@@ -0,0 +1,333 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.time.Year;
+import java.util.Optional;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListSelectionModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.gpx.GpxConstants;
+import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.widgets.JosmTextArea;
+import org.openstreetmap.josm.gui.widgets.JosmTextField;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.io.GpxWriter;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Exports data to a .gpx file. Data may be native GPX or OSM data which will be converted.
+ * @since 1949
+ */
+public class GpxExporter extends FileExporter implements GpxConstants {
+
+    private static final String GPL_WARNING = "<html><font color='red' size='-2'>"
+        + tr("Note: GPL is not compatible with the OSM license. Do not upload GPL licensed tracks.") + "</html>";
+
+    private static final String[] LICENSES = {
+            "Creative Commons By-SA",
+            "Open Database License (ODbL)",
+            "public domain",
+            "GNU Lesser Public License (LGPL)",
+            "BSD License (MIT/X11)"};
+
+    private static final String[] URLS = {
+            "https://creativecommons.org/licenses/by-sa/3.0",
+            "http://opendatacommons.org/licenses/odbl/1.0",
+            "public domain",
+            "https://www.gnu.org/copyleft/lesser.html",
+            "http://www.opensource.org/licenses/bsd-license.php"};
+
+    /**
+     * Constructs a new {@code GpxExporter}.
+     */
+    public GpxExporter() {
+        super(GpxImporter.getFileFilter());
+    }
+
+    @Override
+    public boolean acceptFile(File pathname, Layer layer) {
+        if (!(layer instanceof OsmDataLayer) && !(layer instanceof GpxLayer))
+            return false;
+        return super.acceptFile(pathname, layer);
+    }
+
+    @Override
+    public void exportData(File file, Layer layer) throws IOException {
+        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
+        if (!(layer instanceof OsmDataLayer) && !(layer instanceof GpxLayer))
+            throw new IllegalArgumentException(MessageFormat.format("Expected instance of OsmDataLayer or GpxLayer. Got ''{0}''.", layer
+                    .getClass().getName()));
+        CheckParameterUtil.ensureParameterNotNull(file, "file");
+
+        String fn = file.getPath();
+        if (fn.indexOf('.') == -1) {
+            fn += ".gpx";
+            file = new File(fn);
+        }
+
+        // open the dialog asking for options
+        JPanel p = new JPanel(new GridBagLayout());
+
+        GpxData gpxData;
+        // At this moment, we only need to know the attributes of the GpxData,
+        // conversion of OsmDataLayer (if needed) will be done after the dialog is closed.
+        if (layer instanceof GpxLayer) {
+            gpxData = ((GpxLayer) layer).data;
+        } else {
+            gpxData = new GpxData();
+        }
+
+        p.add(new JLabel(tr("GPS track description")), GBC.eol());
+        JosmTextArea desc = new JosmTextArea(3, 40);
+        desc.setWrapStyleWord(true);
+        desc.setLineWrap(true);
+        desc.setText(gpxData.getString(META_DESC));
+        p.add(new JScrollPane(desc), GBC.eop().fill(GBC.BOTH));
+
+        JCheckBox author = new JCheckBox(tr("Add author information"), Main.pref.getBoolean("lastAddAuthor", true));
+        p.add(author, GBC.eol());
+
+        JLabel nameLabel = new JLabel(tr("Real name"));
+        p.add(nameLabel, GBC.std().insets(10, 0, 5, 0));
+        JosmTextField authorName = new JosmTextField();
+        p.add(authorName, GBC.eol().fill(GBC.HORIZONTAL));
+        nameLabel.setLabelFor(authorName);
+
+        JLabel emailLabel = new JLabel(tr("E-Mail"));
+        p.add(emailLabel, GBC.std().insets(10, 0, 5, 0));
+        JosmTextField email = new JosmTextField();
+        p.add(email, GBC.eol().fill(GBC.HORIZONTAL));
+        emailLabel.setLabelFor(email);
+
+        JLabel copyrightLabel = new JLabel(tr("Copyright (URL)"));
+        p.add(copyrightLabel, GBC.std().insets(10, 0, 5, 0));
+        JosmTextField copyright = new JosmTextField();
+        p.add(copyright, GBC.std().fill(GBC.HORIZONTAL));
+        copyrightLabel.setLabelFor(copyright);
+
+        JButton predefined = new JButton(tr("Predefined"));
+        p.add(predefined, GBC.eol().insets(5, 0, 0, 0));
+
+        JLabel copyrightYearLabel = new JLabel(tr("Copyright year"));
+        p.add(copyrightYearLabel, GBC.std().insets(10, 0, 5, 5));
+        JosmTextField copyrightYear = new JosmTextField("");
+        p.add(copyrightYear, GBC.eol().fill(GBC.HORIZONTAL));
+        copyrightYearLabel.setLabelFor(copyrightYear);
+
+        JLabel warning = new JLabel("<html><font size='-2'>&nbsp;</html");
+        p.add(warning, GBC.eol().fill(GBC.HORIZONTAL).insets(15, 0, 0, 0));
+        addDependencies(gpxData, author, authorName, email, copyright, predefined, copyrightYear, nameLabel, emailLabel,
+                copyrightLabel, copyrightYearLabel, warning);
+
+        p.add(new JLabel(tr("Keywords")), GBC.eol());
+        JosmTextField keywords = new JosmTextField();
+        keywords.setText(gpxData.getString(META_KEYWORDS));
+        p.add(keywords, GBC.eop().fill(GBC.HORIZONTAL));
+
+        ExtendedDialog ed = new ExtendedDialog(Main.parent,
+                tr("Export options"),
+                tr("Export and Save"), tr("Cancel"))
+            .setButtonIcons("exportgpx", "cancel")
+            .setContent(p);
+
+        if (ed.showDialog().getValue() != 1) {
+            setCanceled(true);
+            return;
+        }
+        setCanceled(false);
+
+        Main.pref.put("lastAddAuthor", author.isSelected());
+        if (!authorName.getText().isEmpty()) {
+            Main.pref.put("lastAuthorName", authorName.getText());
+        }
+        if (!copyright.getText().isEmpty()) {
+            Main.pref.put("lastCopyright", copyright.getText());
+        }
+
+        if (layer instanceof OsmDataLayer) {
+            gpxData = ((OsmDataLayer) layer).toGpxData();
+        } else if (layer instanceof GpxLayer) {
+            gpxData = ((GpxLayer) layer).data;
+        } else {
+            gpxData = OsmDataLayer.toGpxData(MainApplication.getLayerManager().getEditDataSet(), file);
+        }
+
+        // add author and copyright details to the gpx data
+        if (author.isSelected()) {
+            if (!authorName.getText().isEmpty()) {
+                gpxData.put(META_AUTHOR_NAME, authorName.getText());
+                gpxData.put(META_COPYRIGHT_AUTHOR, authorName.getText());
+            }
+            if (!email.getText().isEmpty()) {
+                gpxData.put(META_AUTHOR_EMAIL, email.getText());
+            }
+            if (!copyright.getText().isEmpty()) {
+                gpxData.put(META_COPYRIGHT_LICENSE, copyright.getText());
+            }
+            if (!copyrightYear.getText().isEmpty()) {
+                gpxData.put(META_COPYRIGHT_YEAR, copyrightYear.getText());
+            }
+        }
+
+        // add the description to the gpx data
+        if (!desc.getText().isEmpty()) {
+            gpxData.put(META_DESC, desc.getText());
+        }
+
+        // add keywords to the gpx data
+        if (!keywords.getText().isEmpty()) {
+            gpxData.put(META_KEYWORDS, keywords.getText());
+        }
+
+        try (OutputStream fo = Compression.getCompressedFileOutputStream(file)) {
+            new GpxWriter(fo).write(gpxData);
+            fo.flush();
+        } catch (IOException ex) {
+            Logging.error(ex);
+            JOptionPane.showMessageDialog(Main.parent, tr("Error while exporting {0}:\n{1}", fn, ex.getMessage()),
+                    tr("Error"), JOptionPane.ERROR_MESSAGE);
+        }
+    }
+
+    private static void enableCopyright(final GpxData data, final JosmTextField copyright, final JButton predefined,
+            final JosmTextField copyrightYear, final JLabel copyrightLabel, final JLabel copyrightYearLabel,
+            final JLabel warning, boolean enable) {
+        copyright.setEnabled(enable);
+        predefined.setEnabled(enable);
+        copyrightYear.setEnabled(enable);
+        copyrightLabel.setEnabled(enable);
+        copyrightYearLabel.setEnabled(enable);
+        warning.setText(enable ? GPL_WARNING : "<html><font size='-2'>&nbsp;</html");
+
+        if (enable) {
+            if (copyrightYear.getText().isEmpty()) {
+                copyrightYear.setText(Optional.ofNullable(data.getString(META_COPYRIGHT_YEAR)).orElseGet(
+                        () -> Year.now().toString()));
+            }
+            if (copyright.getText().isEmpty()) {
+                copyright.setText(Optional.ofNullable(data.getString(META_COPYRIGHT_LICENSE)).orElseGet(
+                        () -> Main.pref.get("lastCopyright", "https://creativecommons.org/licenses/by-sa/2.5")));
+                copyright.setCaretPosition(0);
+            }
+        } else {
+            copyrightYear.setText("");
+            copyright.setText("");
+        }
+    }
+
+    // CHECKSTYLE.OFF: ParameterNumber
+
+    /**
+     * Add all those listeners to handle the enable state of the fields.
+     * @param data GPX data
+     * @param author Author checkbox
+     * @param authorName Author name textfield
+     * @param email E-mail textfield
+     * @param copyright Copyright textfield
+     * @param predefined Predefined button
+     * @param copyrightYear Copyright year textfield
+     * @param nameLabel Name label
+     * @param emailLabel E-mail label
+     * @param copyrightLabel Copyright label
+     * @param copyrightYearLabel Copyright year label
+     * @param warning Warning label
+     */
+    private static void addDependencies(
+            final GpxData data,
+            final JCheckBox author,
+            final JosmTextField authorName,
+            final JosmTextField email,
+            final JosmTextField copyright,
+            final JButton predefined,
+            final JosmTextField copyrightYear,
+            final JLabel nameLabel,
+            final JLabel emailLabel,
+            final JLabel copyrightLabel,
+            final JLabel copyrightYearLabel,
+            final JLabel warning) {
+
+        // CHECKSTYLE.ON: ParameterNumber
+        ActionListener authorActionListener = e -> {
+            boolean b = author.isSelected();
+            authorName.setEnabled(b);
+            email.setEnabled(b);
+            nameLabel.setEnabled(b);
+            emailLabel.setEnabled(b);
+            if (b) {
+                authorName.setText(Optional.ofNullable(data.getString(META_AUTHOR_NAME)).orElseGet(() -> Main.pref.get("lastAuthorName")));
+                email.setText(Optional.ofNullable(data.getString(META_AUTHOR_EMAIL)).orElseGet(() -> Main.pref.get("lastAuthorEmail")));
+            } else {
+                authorName.setText("");
+                email.setText("");
+            }
+            boolean isAuthorSet = !authorName.getText().isEmpty();
+            GpxExporter.enableCopyright(data, copyright, predefined, copyrightYear, copyrightLabel, copyrightYearLabel, warning,
+                    b && isAuthorSet);
+        };
+        author.addActionListener(authorActionListener);
+
+        KeyAdapter authorNameListener = new KeyAdapter() {
+            @Override public void keyReleased(KeyEvent e) {
+                boolean b = !authorName.getText().isEmpty() && author.isSelected();
+                GpxExporter.enableCopyright(data, copyright, predefined, copyrightYear, copyrightLabel, copyrightYearLabel, warning, b);
+            }
+        };
+        authorName.addKeyListener(authorNameListener);
+
+        predefined.addActionListener(e -> {
+            JList<String> l = new JList<>(LICENSES);
+            l.setVisibleRowCount(LICENSES.length);
+            l.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
+            int answer = JOptionPane.showConfirmDialog(
+                    Main.parent,
+                    new JScrollPane(l),
+                    tr("Choose a predefined license"),
+                    JOptionPane.OK_CANCEL_OPTION,
+                    JOptionPane.QUESTION_MESSAGE
+            );
+            if (answer != JOptionPane.OK_OPTION || l.getSelectedIndex() == -1)
+                return;
+            StringBuilder license = new StringBuilder();
+            for (int i : l.getSelectedIndices()) {
+                if (i == 2) {
+                    license = new StringBuilder("public domain");
+                    break;
+                }
+                if (license.length() > 0) {
+                    license.append(", ");
+                }
+                license.append(URLS[i]);
+            }
+            copyright.setText(license.toString());
+            copyright.setCaretPosition(0);
+        });
+
+        authorActionListener.actionPerformed(null);
+        authorNameListener.keyReleased(null);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxImporter.java	(revision 12671)
@@ -0,0 +1,194 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.io.GpxReader;
+import org.openstreetmap.josm.tools.Logging;
+import org.xml.sax.SAXException;
+
+/**
+ * File importer allowing to import GPX files (*.gpx/gpx.gz files).
+ *
+ */
+public class GpxImporter extends FileImporter {
+
+    /**
+     * Utility class containing imported GPX and marker layers, and a task to run after they are added to MapView.
+     */
+    public static class GpxImporterData {
+        /**
+         * The imported GPX layer. May be null if no GPX data.
+         */
+        private final GpxLayer gpxLayer;
+        /**
+         * The imported marker layer. May be null if no marker.
+         */
+        private final MarkerLayer markerLayer;
+        /**
+         * The task to run after GPX and/or marker layer has been added to MapView.
+         */
+        private final Runnable postLayerTask;
+
+        /**
+         * Constructs a new {@code GpxImporterData}.
+         * @param gpxLayer The imported GPX layer. May be null if no GPX data.
+         * @param markerLayer The imported marker layer. May be null if no marker.
+         * @param postLayerTask The task to run after GPX and/or marker layer has been added to MapView.
+         */
+        public GpxImporterData(GpxLayer gpxLayer, MarkerLayer markerLayer, Runnable postLayerTask) {
+            this.gpxLayer = gpxLayer;
+            this.markerLayer = markerLayer;
+            this.postLayerTask = postLayerTask;
+        }
+
+        /**
+         * Returns the imported GPX layer. May be null if no GPX data.
+         * @return the imported GPX layer. May be null if no GPX data.
+         */
+        public GpxLayer getGpxLayer() {
+            return gpxLayer;
+        }
+
+        /**
+         * Returns the imported marker layer. May be null if no marker.
+         * @return the imported marker layer. May be null if no marker.
+         */
+        public MarkerLayer getMarkerLayer() {
+            return markerLayer;
+        }
+
+        /**
+         * Returns the task to run after GPX and/or marker layer has been added to MapView.
+         * @return the task to run after GPX and/or marker layer has been added to MapView.
+         */
+        public Runnable getPostLayerTask() {
+            return postLayerTask;
+        }
+    }
+
+    /**
+     * Constructs a new {@code GpxImporter}.
+     */
+    public GpxImporter() {
+        super(getFileFilter());
+    }
+
+    /**
+     * Returns a GPX file filter (*.gpx and *.gpx.gz files).
+     * @return a GPX file filter
+     */
+    public static ExtensionFileFilter getFileFilter() {
+        return ExtensionFileFilter.newFilterWithArchiveExtensions(
+            "gpx", Main.pref.get("save.extension.gpx", "gpx"), tr("GPX Files"), true);
+    }
+
+    @Override
+    public void importData(File file, ProgressMonitor progressMonitor) throws IOException {
+        final String fileName = file.getName();
+
+        try (InputStream is = Compression.getUncompressedFileInputStream(file)) {
+            GpxReader r = new GpxReader(is);
+            boolean parsedProperly = r.parse(true);
+            r.getGpxData().storageFile = file;
+            addLayers(loadLayers(r.getGpxData(), parsedProperly, fileName, tr("Markers from {0}", fileName)));
+        } catch (SAXException e) {
+            Logging.error(e);
+            throw new IOException(tr("Parsing data for layer ''{0}'' failed", fileName), e);
+        }
+    }
+
+    /**
+     * Adds the specified GPX and marker layers to Map.main
+     * @param data The layers to add
+     * @see #loadLayers
+     */
+    public static void addLayers(final GpxImporterData data) {
+        // FIXME: remove UI stuff from the IO subsystem
+        GuiHelper.runInEDT(() -> {
+            if (data.markerLayer != null) {
+                MainApplication.getLayerManager().addLayer(data.markerLayer);
+            }
+            if (data.gpxLayer != null) {
+                MainApplication.getLayerManager().addLayer(data.gpxLayer);
+            }
+            data.postLayerTask.run();
+        });
+    }
+
+    /**
+     * Replies the new GPX and marker layers corresponding to the specified GPX data.
+     * @param data The GPX data
+     * @param parsedProperly True if GPX data has been properly parsed by {@link GpxReader#parse}
+     * @param gpxLayerName The GPX layer name
+     * @param markerLayerName The marker layer name
+     * @return the new GPX and marker layers corresponding to the specified GPX data, to be used with {@link #addLayers}
+     * @see #addLayers
+     */
+    public static GpxImporterData loadLayers(final GpxData data, final boolean parsedProperly,
+            final String gpxLayerName, String markerLayerName) {
+        GpxLayer gpxLayer = null;
+        MarkerLayer markerLayer = null;
+        if (data.hasRoutePoints() || data.hasTrackPoints()) {
+            gpxLayer = new GpxLayer(data, gpxLayerName, data.storageFile != null);
+        }
+        if (Main.pref.getBoolean("marker.makeautomarkers", true) && !data.waypoints.isEmpty()) {
+            markerLayer = new MarkerLayer(data, markerLayerName, data.storageFile, gpxLayer);
+            if (markerLayer.data.isEmpty()) {
+                markerLayer = null;
+            }
+        }
+        Runnable postLayerTask = () -> {
+            if (!parsedProperly) {
+                String msg;
+                if (data.storageFile == null) {
+                    msg = tr("Error occurred while parsing gpx data for layer ''{0}''. Only a part of the file will be available.",
+                            gpxLayerName);
+                } else {
+                    msg = tr("Error occurred while parsing gpx file ''{0}''. Only a part of the file will be available.",
+                            data.storageFile.getPath());
+                }
+                JOptionPane.showMessageDialog(null, msg);
+            }
+        };
+        return new GpxImporterData(gpxLayer, markerLayer, postLayerTask);
+    }
+
+    /**
+     * Replies the new GPX and marker layers corresponding to the specified GPX file.
+     * @param is input stream to GPX data
+     * @param associatedFile GPX file
+     * @param gpxLayerName The GPX layer name
+     * @param markerLayerName The marker layer name
+     * @param progressMonitor The progress monitor
+     * @return the new GPX and marker layers corresponding to the specified GPX file
+     * @throws IOException if an I/O error occurs
+     */
+    public static GpxImporterData loadLayers(InputStream is, final File associatedFile,
+            final String gpxLayerName, String markerLayerName, ProgressMonitor progressMonitor) throws IOException {
+        try {
+            final GpxReader r = new GpxReader(is);
+            final boolean parsedProperly = r.parse(true);
+            r.getGpxData().storageFile = associatedFile;
+            return loadLayers(r.getGpxData(), parsedProperly, gpxLayerName, markerLayerName);
+        } catch (SAXException e) {
+            Logging.error(e);
+            throw new IOException(tr("Parsing data for layer ''{0}'' failed", gpxLayerName), e);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/JpgImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/JpgImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/JpgImporter.java	(revision 12671)
@@ -0,0 +1,132 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.IllegalDataException;
+
+/**
+ * File importer allowing to import geottaged images (*.jpg files).
+ *
+ */
+public class JpgImporter extends FileImporter {
+    private GpxLayer gpx;
+
+    /**
+     * The default file filter (only *.jpg files).
+     */
+    public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter(
+            "jpg,jpeg", "jpg", tr("Image Files") + " (*.jpg)");
+
+    /**
+     * An alternate file filter that also includes folders.
+     * @since 5438
+     */
+    public static final ExtensionFileFilter FILE_FILTER_WITH_FOLDERS = new ExtensionFileFilter(
+            "jpg,jpeg", "jpg", tr("Image Files") + " (*.jpg, "+ tr("folder")+')');
+
+    /**
+     * Constructs a new {@code JpgImporter}.
+     */
+    public JpgImporter() {
+        this(false);
+    }
+
+    /**
+     * Constructs a new {@code JpgImporter} with folders selection, if wanted.
+     * @param includeFolders If true, includes folders in the file filter
+     * @since 5438
+     */
+    public JpgImporter(boolean includeFolders) {
+        super(includeFolders ? FILE_FILTER_WITH_FOLDERS : FILE_FILTER);
+    }
+
+    /**
+     * Constructs a new {@code JpgImporter} for the given GPX layer. Folders selection is allowed.
+     * @param gpx The GPX layer
+     */
+    public JpgImporter(GpxLayer gpx) {
+        this(true);
+        this.gpx = gpx;
+    }
+
+    @Override
+    public boolean acceptFile(File pathname) {
+        return super.acceptFile(pathname) || pathname.isDirectory();
+    }
+
+    @Override
+    public void importData(List<File> sel, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
+        progressMonitor.beginTask(tr("Looking for image files"), 1);
+        try {
+            List<File> files = new ArrayList<>();
+            Set<String> visitedDirs = new HashSet<>();
+            addRecursiveFiles(files, visitedDirs, sel, progressMonitor.createSubTaskMonitor(1, true));
+
+            if (progressMonitor.isCanceled())
+                return;
+
+            if (files.isEmpty())
+                throw new IOException(tr("No image files found."));
+
+            GeoImageLayer.create(files, gpx);
+        } finally {
+            progressMonitor.finishTask();
+        }
+    }
+
+    static void addRecursiveFiles(List<File> files, Set<String> visitedDirs, List<File> sel, ProgressMonitor progressMonitor)
+            throws IOException {
+
+        if (progressMonitor.isCanceled())
+            return;
+
+        progressMonitor.beginTask(null, sel.size());
+        try {
+            for (File f : sel) {
+                if (f.isDirectory()) {
+                    if (visitedDirs.add(f.getCanonicalPath())) { // Do not loop over symlinks
+                        File[] dirFiles = f.listFiles(); // Can be null for some strange directories (like lost+found)
+                        if (dirFiles != null) {
+                            addRecursiveFiles(files, visitedDirs, Arrays.asList(dirFiles), progressMonitor.createSubTaskMonitor(1, true));
+                        }
+                    } else {
+                        progressMonitor.worked(1);
+                    }
+                } else {
+                    if (FILE_FILTER.accept(f)) {
+                        files.add(f);
+                    }
+                    progressMonitor.worked(1);
+                }
+            }
+        } finally {
+            progressMonitor.finishTask();
+        }
+    }
+
+    @Override
+    public boolean isBatchImporter() {
+        return true;
+    }
+
+    /**
+     * Needs to be the last, to avoid problems.
+     */
+    @Override
+    public double getPriority() {
+        return -1000;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/NMEAImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/NMEAImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/NMEAImporter.java	(revision 12671)
@@ -0,0 +1,109 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.swing.JOptionPane;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.HelpAwareOptionPane;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.Notification;
+import org.openstreetmap.josm.gui.io.importexport.GpxImporter.GpxImporterData;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.io.nmea.NmeaReader;
+
+/**
+ * File importer allowing to import NMEA-0183 files (*.nmea/nme/nma/log/txt files).
+ * @since 1637
+ */
+public class NMEAImporter extends FileImporter {
+
+    /**
+     * The NMEA file filter (*.nmea *.nme *.nma *.log *.txt files).
+     */
+    public static final ExtensionFileFilter FILE_FILTER = ExtensionFileFilter.newFilterWithArchiveExtensions(
+            "nmea,nme,nma,log,txt", "nmea", tr("NMEA-0183 Files"), false);
+
+    /**
+     * Constructs a new {@code NMEAImporter}.
+     */
+    public NMEAImporter() {
+        super(FILE_FILTER);
+    }
+
+    @Override
+    public void importData(File file, ProgressMonitor progressMonitor) throws IOException {
+        final String fn = file.getName();
+        try (InputStream fis = Compression.getUncompressedFileInputStream(file)) {
+            final NmeaReader r = new NmeaReader(fis);
+            if (r.getNumberOfCoordinates() > 0) {
+                r.data.storageFile = file;
+                final GpxLayer gpxLayer = new GpxLayer(r.data, fn, true);
+                final File fileFinal = file;
+
+                GuiHelper.runInEDT(() -> {
+                    MainApplication.getLayerManager().addLayer(gpxLayer);
+                    if (Main.pref.getBoolean("marker.makeautomarkers", true)) {
+                        MarkerLayer ml = new MarkerLayer(r.data, tr("Markers from {0}", fn), fileFinal, gpxLayer);
+                        if (!ml.data.isEmpty()) {
+                            MainApplication.getLayerManager().addLayer(ml);
+                        }
+                    }
+                });
+            }
+            showNmeaInfobox(r.getNumberOfCoordinates() > 0, r);
+        }
+    }
+
+    private static void showNmeaInfobox(boolean success, NmeaReader r) {
+        final StringBuilder msg = new StringBuilder(160).append("<html>")
+           .append(tr("Coordinates imported: {0}", r.getNumberOfCoordinates())).append("<br>")
+           .append(tr("Malformed sentences: {0}", r.getParserMalformed())).append("<br>")
+           .append(tr("Checksum errors: {0}", r.getParserChecksumErrors())).append("<br>");
+        if (!success) {
+            msg.append(tr("Unknown sentences: {0}", r.getParserUnknown())).append("<br>");
+        }
+        msg.append(tr("Zero coordinates: {0}", r.getParserZeroCoordinates()))
+           .append("</html>");
+        if (success) {
+            SwingUtilities.invokeLater(() -> new Notification(
+                    "<h3>" + tr("NMEA import success:") + "</h3>" + msg.toString())
+                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
+                    .show());
+        } else {
+            HelpAwareOptionPane.showMessageDialogInEDT(
+                    Main.parent,
+                    msg.toString(),
+                    tr("NMEA import failure!"),
+                    JOptionPane.ERROR_MESSAGE, null);
+        }
+    }
+
+    /**
+     * Replies the new GPX and marker layers corresponding to the specified NMEA file.
+     * @param is input stream to NMEA 0183 data
+     * @param associatedFile NMEA file
+     * @param gpxLayerName The GPX layer name
+     * @param markerLayerName The marker layer name
+     * @return the new GPX and marker layers corresponding to the specified NMEA file
+     * @throws IOException if an I/O error occurs
+     */
+    public static GpxImporterData loadLayers(InputStream is, final File associatedFile,
+            final String gpxLayerName, String markerLayerName) throws IOException {
+        final NmeaReader r = new NmeaReader(is);
+        final boolean parsedProperly = r.getNumberOfCoordinates() > 0;
+        r.data.storageFile = associatedFile;
+        return GpxImporter.loadLayers(r.data, parsedProperly, gpxLayerName, markerLayerName);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/NoteExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/NoteExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/NoteExporter.java	(revision 12671)
@@ -0,0 +1,48 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.NoteLayer;
+import org.openstreetmap.josm.io.NoteWriter;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Exporter to write note data to an XML file
+ */
+public class NoteExporter extends FileExporter {
+
+    /** File extension filter for .osn files */
+    public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter(
+            "osn", "osn", tr("Note Files") + " (*.osn)");
+
+    /** Create a new note exporter with the default .osn file filter */
+    public NoteExporter() {
+        super(FILE_FILTER);
+    }
+
+    @Override
+    public boolean acceptFile(File pathname, Layer layer) {
+        if (!(layer instanceof NoteLayer))
+            return false;
+        return super.acceptFile(pathname, layer);
+    }
+
+    @Override
+    public void exportData(File file, Layer layer) throws IOException {
+        Logging.info("exporting notes to file: " + file);
+        if (layer instanceof NoteLayer) {
+            try (OutputStream os = new FileOutputStream(file);
+                 NoteWriter writer = new NoteWriter(os)) {
+                writer.write(((NoteLayer) layer).getNoteData());
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/NoteImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/NoteImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/NoteImporter.java	(revision 12671)
@@ -0,0 +1,79 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.data.notes.Note;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.NoteLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.io.NoteReader;
+import org.openstreetmap.josm.tools.Logging;
+import org.xml.sax.SAXException;
+
+/**
+ * File importer that reads note dump files (*.osn, .osn.gz and .osn.bz2)
+ * @since 7538
+ */
+public class NoteImporter extends FileImporter {
+
+    private static final ExtensionFileFilter FILE_FILTER = ExtensionFileFilter.newFilterWithArchiveExtensions(
+            "osn", "osn", tr("Note Files"), true);
+
+    /** Create an importer for note dump files */
+    public NoteImporter() {
+        super(FILE_FILTER);
+    }
+
+    @Override
+    public void importData(final File file, ProgressMonitor progressMonitor) throws IOException {
+        if (Logging.isDebugEnabled()) {
+            Logging.debug("importing notes file {0}", file.getAbsolutePath());
+        }
+        try (InputStream is = Compression.getUncompressedFileInputStream(file)) {
+            final NoteLayer layer = loadLayer(is, file, file.getName(), progressMonitor);
+            if (!MainApplication.getLayerManager().containsLayer(layer)) {
+                MainApplication.getLayerManager().addLayer(layer);
+            }
+        } catch (SAXException e) {
+            Logging.error("error opening up notes file");
+            Logging.error(e);
+            throw new IOException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Load note layer from InputStream.
+     * @param in input stream
+     * @param associatedFile filename of data (can be <code>null</code> if the stream does not come from a file)
+     * @param layerName name of generated layer
+     * @param progressMonitor handler for progress monitoring and canceling
+     * @return note layer
+     * @throws IOException if any I/O error occurs
+     * @throws SAXException if any SAX error occurs
+     * @since 9746
+     */
+    public NoteLayer loadLayer(InputStream in, final File associatedFile, final String layerName, ProgressMonitor progressMonitor)
+            throws SAXException, IOException {
+        final List<Note> fileNotes = new NoteReader(in).parse();
+        List<NoteLayer> noteLayers = null;
+        if (MainApplication.getMap() != null) {
+            noteLayers = MainApplication.getLayerManager().getLayersOfType(NoteLayer.class);
+        }
+        final NoteLayer layer;
+        if (noteLayers != null && !noteLayers.isEmpty()) {
+            layer = noteLayers.get(0);
+            layer.getNoteData().addNotes(fileNotes);
+        } else {
+            layer = new NoteLayer(fileNotes, associatedFile != null ? associatedFile.getName() : tr("Notes"));
+        }
+        return layer;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmBzip2Exporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmBzip2Exporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmBzip2Exporter.java	(revision 12671)
@@ -0,0 +1,23 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+
+/**
+ * OSM data exporter that compresses it in Bzip2 format.
+ */
+public class OsmBzip2Exporter extends OsmExporter {
+
+    /**
+     * Constructs a new {@code OsmBzip2Exporter}.
+     */
+    public OsmBzip2Exporter() {
+        super(new ExtensionFileFilter(
+            "osm.bz2,osm.bz", "osm.bz2", tr("OSM Server Files bzip2 compressed") + " (*.osm.bz2, *.osm.bz)"));
+    }
+
+    // compression handling is performed in super-class
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmChangeImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmChangeImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmChangeImporter.java	(revision 12671)
@@ -0,0 +1,76 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.io.IllegalDataException;
+import org.openstreetmap.josm.io.OsmChangeReader;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * File importer that reads OSM change files (*.osc).
+ * @see <a href="http://wiki.openstreetmap.org/wiki/OsmChange">OsmChange</a>
+ */
+public class OsmChangeImporter extends FileImporter {
+
+    public static final ExtensionFileFilter FILE_FILTER = ExtensionFileFilter.newFilterWithArchiveExtensions(
+            "osc", "osc", tr("OsmChange File"), true);
+
+    /**
+     * Constructs a new {@code OsmChangeImporter}.
+     */
+    public OsmChangeImporter() {
+        super(FILE_FILTER);
+    }
+
+    public OsmChangeImporter(ExtensionFileFilter filter) {
+        super(filter);
+    }
+
+    @Override
+    public void importData(File file, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
+        try {
+            importData(Compression.getUncompressedFileInputStream(file), file, progressMonitor);
+        } catch (FileNotFoundException e) {
+            Logging.error(e);
+            throw new IOException(tr("File ''{0}'' does not exist.", file.getName()), e);
+        }
+    }
+
+    protected void importData(InputStream in, final File associatedFile, ProgressMonitor progressMonitor) throws IllegalDataException {
+        final DataSet dataSet = OsmChangeReader.parseDataSet(in, progressMonitor);
+        final OsmDataLayer layer = new OsmDataLayer(dataSet, associatedFile.getName(), associatedFile);
+        addDataLayer(dataSet, layer, associatedFile.getPath());
+    }
+
+    protected void addDataLayer(final DataSet dataSet, final OsmDataLayer layer, final String filePath) {
+        // FIXME: remove UI stuff from IO subsystem
+        //
+        GuiHelper.runInEDT(() -> {
+            if (dataSet.allPrimitives().isEmpty()) {
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        tr("No data found in file {0}.", filePath),
+                        tr("Open OsmChange file"),
+                        JOptionPane.INFORMATION_MESSAGE);
+            }
+            MainApplication.getLayerManager().addLayer(layer);
+            layer.onPostLoadFromFile();
+        });
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmExporter.java	(revision 12671)
@@ -0,0 +1,139 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.io.OsmWriter;
+import org.openstreetmap.josm.io.OsmWriterFactory;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Exports data to an .osm file.
+ * @since 1949
+ */
+public class OsmExporter extends FileExporter {
+
+    /**
+     * Constructs a new {@code OsmExporter}.
+     */
+    public OsmExporter() {
+        super(new ExtensionFileFilter(
+            "osm,xml", "osm", tr("OSM Server Files") + " (*.osm)"));
+    }
+
+    /**
+     * Constructs a new {@code OsmExporter}.
+     * @param filter The extension file filter
+     */
+    public OsmExporter(ExtensionFileFilter filter) {
+        super(filter);
+    }
+
+    @Override
+    public boolean acceptFile(File pathname, Layer layer) {
+        if (!(layer instanceof OsmDataLayer))
+            return false;
+        return super.acceptFile(pathname, layer);
+    }
+
+    @Override
+    public void exportData(File file, Layer layer) throws IOException {
+        exportData(file, layer, false);
+    }
+
+    /**
+     * Exports OSM data to the given file.
+     * @param file Output file
+     * @param layer Data layer. Must be an instance of {@link OsmDataLayer}.
+     * @param noBackup if {@code true}, the potential backup file created if the output file already exists will be deleted
+     *                 after a successful export
+     * @throws IllegalArgumentException if {@code layer} is not an instance of {@code OsmDataLayer}
+     */
+    public void exportData(File file, Layer layer, boolean noBackup) {
+        if (!(layer instanceof OsmDataLayer)) {
+            throw new IllegalArgumentException(
+                    MessageFormat.format("Expected instance of OsmDataLayer. Got ''{0}''.", layer.getClass().getName()));
+        }
+        save(file, (OsmDataLayer) layer, noBackup);
+    }
+
+    protected static OutputStream getOutputStream(File file) throws IOException {
+        return Compression.getCompressedFileOutputStream(file);
+    }
+
+    private void save(File file, OsmDataLayer layer, boolean noBackup) {
+        File tmpFile = null;
+        try {
+            // use a tmp file because if something errors out in the process of writing the file,
+            // we might just end up with a truncated file.  That can destroy lots of work.
+            if (file.exists()) {
+                tmpFile = new File(file.getPath() + '~');
+                Utils.copyFile(file, tmpFile);
+            }
+
+            doSave(file, layer);
+            if ((noBackup || !Main.pref.getBoolean("save.keepbackup", false)) && tmpFile != null) {
+                Utils.deleteFile(tmpFile);
+            }
+            layer.onPostSaveToFile();
+        } catch (IOException e) {
+            Logging.error(e);
+            JOptionPane.showMessageDialog(
+                    Main.parent,
+                    tr("<html>An error occurred while saving.<br>Error is:<br>{0}</html>",
+                            Utils.escapeReservedCharactersHTML(e.getMessage())),
+                    tr("Error"),
+                    JOptionPane.ERROR_MESSAGE
+            );
+
+            try {
+                // if the file save failed, then the tempfile will not be deleted. So, restore the backup if we made one.
+                if (tmpFile != null && tmpFile.exists()) {
+                    Utils.copyFile(tmpFile, file);
+                }
+            } catch (IOException e2) {
+                Logging.error(e2);
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        tr("<html>An error occurred while restoring backup file.<br>Error is:<br>{0}</html>",
+                                Utils.escapeReservedCharactersHTML(e2.getMessage())),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE
+                );
+            }
+        }
+    }
+
+    protected void doSave(File file, OsmDataLayer layer) throws IOException {
+        // create outputstream and wrap it with gzip or bzip, if necessary
+        try (
+            OutputStream out = getOutputStream(file);
+            Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+            OsmWriter w = OsmWriterFactory.createOsmWriter(new PrintWriter(writer), false, layer.data.getVersion())
+        ) {
+            layer.data.getReadLock().lock();
+            try {
+                w.writeLayer(layer);
+            } finally {
+                layer.data.getReadLock().unlock();
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmGzipExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmGzipExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmGzipExporter.java	(revision 12671)
@@ -0,0 +1,23 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+
+/**
+ * OSM data exporter that compresses it in GZip format.
+ */
+public class OsmGzipExporter extends OsmExporter {
+
+    /**
+     * Constructs a new {@code OsmGzipExporter}.
+     */
+    public OsmGzipExporter() {
+        super(new ExtensionFileFilter(
+            "osm.gz", "osm.gz", tr("OSM Server Files gzip compressed") + " (*.osm.gz)"));
+    }
+
+    // compression handling is performed in super-class
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmImporter.java	(revision 12671)
@@ -0,0 +1,156 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.io.IllegalDataException;
+import org.openstreetmap.josm.io.OsmReader;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * File importer that reads *.osm data files. (main storage format for OSM data in JOSM)
+ */
+public class OsmImporter extends FileImporter {
+
+    /**
+     * The OSM file filter (*.osm and *.xml files).
+     */
+    public static final ExtensionFileFilter FILE_FILTER = ExtensionFileFilter.newFilterWithArchiveExtensions(
+            "osm,xml", "osm", tr("OSM Server Files") + " (*.osm, *.osm.gz, *.osm.bz2, *.osm.zip, *.xml)",
+            ExtensionFileFilter.AddArchiveExtension.NONE, Arrays.asList("gz", "bz", "bz2", "zip"));
+
+    /**
+     * Utility class containing imported OSM layer, and a task to run after it is added to MapView.
+     */
+    public static class OsmImporterData {
+
+        private final OsmDataLayer layer;
+        private final Runnable postLayerTask;
+
+        public OsmImporterData(OsmDataLayer layer, Runnable postLayerTask) {
+            this.layer = layer;
+            this.postLayerTask = postLayerTask;
+        }
+
+        public OsmDataLayer getLayer() {
+            return layer;
+        }
+
+        public Runnable getPostLayerTask() {
+            return postLayerTask;
+        }
+    }
+
+    /**
+     * Constructs a new {@code OsmImporter}.
+     */
+    public OsmImporter() {
+        super(FILE_FILTER);
+    }
+
+    /**
+     * Constructs a new {@code OsmImporter} with the given extension file filter.
+     * @param filter The extension file filter
+     */
+    public OsmImporter(ExtensionFileFilter filter) {
+        super(filter);
+    }
+
+    /**
+     * Imports OSM data from file
+     * @param file file to read data from
+     * @param progressMonitor handler for progress monitoring and canceling
+     */
+    @Override
+    public void importData(File file, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
+        try (InputStream in = Compression.getUncompressedFileInputStream(file)) {
+            importData(in, file, progressMonitor);
+        } catch (FileNotFoundException e) {
+            Logging.error(e);
+            throw new IOException(tr("File ''{0}'' does not exist.", file.getName()), e);
+        }
+    }
+
+    /**
+     * Imports OSM data from stream
+     * @param in input stream
+     * @param associatedFile filename of data (layer name will be generated from name of file)
+     * @param pm handler for progress monitoring and canceling
+     * @throws IllegalDataException if an error was found while parsing the OSM data
+     */
+    protected void importData(InputStream in, final File associatedFile, ProgressMonitor pm) throws IllegalDataException {
+        final OsmImporterData data = loadLayer(in, associatedFile,
+                associatedFile == null ? OsmDataLayer.createNewName() : associatedFile.getName(), pm);
+
+        // FIXME: remove UI stuff from IO subsystem
+        GuiHelper.runInEDT(() -> {
+            OsmDataLayer layer = data.getLayer();
+            MainApplication.getLayerManager().addLayer(layer);
+            data.getPostLayerTask().run();
+            data.getLayer().onPostLoadFromFile();
+        });
+    }
+
+    /**
+     * Load osm data layer from InputStream.
+     * @param in input stream
+     * @param associatedFile filename of data (can be <code>null</code> if the stream does not come from a file)
+     * @param layerName name of generated layer
+     * @param progressMonitor handler for progress monitoring and canceling
+     * @return Utility class containing imported OSM layer, and a task to run after it is added to MapView
+     * @throws IllegalDataException if an error was found while parsing the OSM data
+     */
+    public OsmImporterData loadLayer(InputStream in, final File associatedFile, final String layerName, ProgressMonitor progressMonitor)
+            throws IllegalDataException {
+        final DataSet dataSet = parseDataSet(in, progressMonitor);
+        if (dataSet == null) {
+            throw new IllegalDataException(tr("Invalid dataset"));
+        }
+        OsmDataLayer layer = createLayer(dataSet, associatedFile, layerName);
+        Runnable postLayerTask = createPostLayerTask(dataSet, associatedFile, layerName, layer);
+        return new OsmImporterData(layer, postLayerTask);
+    }
+
+    protected DataSet parseDataSet(InputStream in, ProgressMonitor progressMonitor) throws IllegalDataException {
+        return OsmReader.parseDataSet(in, progressMonitor);
+    }
+
+    protected OsmDataLayer createLayer(final DataSet dataSet, final File associatedFile, final String layerName) {
+        return new OsmDataLayer(dataSet, layerName, associatedFile);
+    }
+
+    protected Runnable createPostLayerTask(final DataSet dataSet, final File associatedFile, final String layerName, final OsmDataLayer layer) {
+        return () -> {
+            if (dataSet.allPrimitives().isEmpty()) {
+                String msg;
+                if (associatedFile == null) {
+                    msg = tr("No data found for layer ''{0}''.", layerName);
+                } else {
+                    msg = tr("No data found in file ''{0}''.", associatedFile.getPath());
+                }
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        msg,
+                        tr("Open OSM file"),
+                        JOptionPane.INFORMATION_MESSAGE);
+            }
+            layer.onPostLoadFromFile();
+        };
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/ValidatorErrorExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/ValidatorErrorExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/ValidatorErrorExporter.java	(revision 12671)
@@ -0,0 +1,52 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.ValidatorLayer;
+import org.openstreetmap.josm.io.ValidatorErrorWriter;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Exporter to write validator errors to an XML file.
+ * @since 12667
+ */
+public class ValidatorErrorExporter extends FileExporter {
+
+    /** File extension filter for .xml files */
+    public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter(
+            "xml", "xml", tr("Validation Error Files") + " (*.xml)");
+
+    /** Create a new validator error exporter with the default .xml file filter */
+    public ValidatorErrorExporter() {
+        super(FILE_FILTER);
+    }
+
+    @Override
+    public boolean acceptFile(File pathname, Layer layer) {
+        if (!(layer instanceof ValidatorLayer))
+            return false;
+        return super.acceptFile(pathname, layer);
+    }
+
+    @Override
+    public void exportData(File file, Layer layer) throws IOException {
+        OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
+        if (layer instanceof ValidatorLayer && editLayer != null) {
+            Logging.info("exporting validation errors to file: " + file);
+            try (OutputStream os = new FileOutputStream(file);
+                 ValidatorErrorWriter writer = new ValidatorErrorWriter(os)) {
+                writer.write(editLayer.validationErrors);
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/WMSLayerExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/WMSLayerExporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/WMSLayerExporter.java	(revision 12671)
@@ -0,0 +1,53 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * Export a WMS layer to a serialized binary file that can be imported later via {@link WMSLayerImporter}.
+ *
+ * @since 5457
+ */
+public class WMSLayerExporter extends FileExporter {
+
+    /** Which version of the file we export */
+    public static final int CURRENT_FILE_VERSION = 6;
+
+    /**
+     * Constructs a new {@code WMSLayerExporter}
+     */
+    public WMSLayerExporter() {
+        super(WMSLayerImporter.FILE_FILTER);
+    }
+
+    @Override
+    public void exportData(File file, Layer layer) throws IOException {
+        CheckParameterUtil.ensureParameterNotNull(file, "file");
+        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
+
+        if (layer instanceof AbstractTileSourceLayer) {
+            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
+                oos.writeInt(CURRENT_FILE_VERSION); // file version
+                oos.writeObject(MainApplication.getMap().mapView.getCenter());
+                ImageryPreferenceEntry entry = new ImageryPreferenceEntry(((AbstractTileSourceLayer) layer).getInfo());
+                oos.writeObject(Preferences.serializeStruct(entry, ImageryPreferenceEntry.class));
+            }
+        }
+    }
+
+    @Override
+    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
+        setEnabled(e.getSource().getActiveLayer() instanceof AbstractTileSourceLayer);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/WMSLayerImporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/WMSLayerImporter.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/WMSLayerImporter.java	(revision 12671)
@@ -0,0 +1,95 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io.importexport;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InvalidClassException;
+import java.io.ObjectInputStream;
+import java.util.Map;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.IllegalDataException;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * Import a WMS layer from a serialized binary file previously exported via {@link WMSLayerExporter}.
+ * @since 5457
+ */
+public class WMSLayerImporter extends FileImporter {
+
+    /**
+     * The file filter used in "open" and "save" dialogs for WMS layers.
+     */
+    public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter(
+            "wms", "wms", tr("WMS Files (*.wms)"));
+
+    /**
+     * Constructs a new {@code WMSLayerImporter}.
+     */
+    public WMSLayerImporter() {
+        super(FILE_FILTER);
+    }
+
+    @Override
+    public void importData(File file, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
+        CheckParameterUtil.ensureParameterNotNull(file, "file");
+        final EastNorth zoomTo;
+        ImageryInfo info = null;
+        final ImageryLayer layer;
+
+        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
+            int sfv = ois.readInt();
+            if (sfv < 5) {
+                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 5));
+            } else if (sfv == 5) {
+                ois.readInt(); // dax - not needed
+                ois.readInt(); // day - not needed
+                zoomTo = null;
+
+                int imageSize = ois.readInt();
+                double pixelPerDegree = ois.readDouble();
+
+                String name = (String) ois.readObject();
+                String extendedUrl = (String) ois.readObject();
+
+                info = new ImageryInfo(name);
+                info.setExtendedUrl(extendedUrl);
+                info.setPixelPerDegree(pixelPerDegree);
+                info.setTileSize(imageSize);
+            } else if (sfv == WMSLayerExporter.CURRENT_FILE_VERSION) {
+                zoomTo = (EastNorth) ois.readObject();
+
+                @SuppressWarnings("unchecked")
+                ImageryPreferenceEntry entry = Preferences.deserializeStruct(
+                        (Map<String, String>) ois.readObject(),
+                        ImageryPreferenceEntry.class);
+                info = new ImageryInfo(entry);
+            } else {
+                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 6));
+            }
+        } catch (ClassNotFoundException e) {
+            throw new IllegalDataException(e);
+        }
+        layer = ImageryLayer.create(info);
+
+
+        // FIXME: remove UI stuff from IO subsystem
+        GuiHelper.runInEDT(() -> {
+            MainApplication.getLayerManager().addLayer(layer);
+            if (zoomTo != null) {
+                MainApplication.getMap().mapView.zoomTo(zoomTo);
+            }
+        });
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/importexport/package-info.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/importexport/package-info.java	(revision 12671)
+++ trunk/src/org/openstreetmap/josm/gui/io/importexport/package-info.java	(revision 12671)
@@ -0,0 +1,6 @@
+// License: GPL. For details, see LICENSE file.
+
+/**
+ * Provides GUI classes for handling file import/export operations.
+ */
+package org.openstreetmap.josm.gui.io.importexport;
