Ticket #21813: 21813-21923-v2.1.patch

File 21813-21923-v2.1.patch, 69.4 KB (added by Bjoeni, 4 years ago)
  • src/org/openstreetmap/josm/actions/SessionLoadAction.java

     
    205205                    postLoadTasks = reader.getPostLoadTasks();
    206206                    viewport = reader.getViewport();
    207207                    projectionChoice = reader.getProjectionChoice();
     208                    SessionSaveAction.setCurrentSession(file, zip, reader.getLayers());
    208209                } finally {
    209210                    if (tempFile) {
    210211                        Utils.deleteFile(file);
  • src/org/openstreetmap/josm/actions/SessionSaveAction.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.actions;
     3
     4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
     5import static org.openstreetmap.josm.tools.I18n.tr;
     6import static org.openstreetmap.josm.tools.I18n.trn;
     7
     8import java.awt.Component;
     9import java.awt.Dimension;
     10import java.awt.GridBagLayout;
     11import java.awt.event.ActionEvent;
     12import java.awt.event.KeyEvent;
     13import java.io.File;
     14import java.io.IOException;
     15import java.lang.ref.WeakReference;
     16import java.util.ArrayList;
     17import java.util.Arrays;
     18import java.util.Collection;
     19import java.util.HashMap;
     20import java.util.HashSet;
     21import java.util.List;
     22import java.util.Map;
     23import java.util.Objects;
     24import java.util.Set;
     25import java.util.stream.Collectors;
     26import java.util.stream.Stream;
     27
     28import javax.swing.BorderFactory;
     29import javax.swing.JCheckBox;
     30import javax.swing.JFileChooser;
     31import javax.swing.JLabel;
     32import javax.swing.JOptionPane;
     33import javax.swing.JPanel;
     34import javax.swing.JScrollPane;
     35import javax.swing.JTabbedPane;
     36import javax.swing.SwingConstants;
     37import javax.swing.border.EtchedBorder;
     38import javax.swing.filechooser.FileFilter;
     39
     40import org.openstreetmap.josm.data.PreferencesUtils;
     41import org.openstreetmap.josm.data.preferences.BooleanProperty;
     42import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
     43import org.openstreetmap.josm.gui.ExtendedDialog;
     44import org.openstreetmap.josm.gui.HelpAwareOptionPane;
     45import org.openstreetmap.josm.gui.MainApplication;
     46import org.openstreetmap.josm.gui.MapFrame;
     47import org.openstreetmap.josm.gui.MapFrameListener;
     48import org.openstreetmap.josm.gui.Notification;
     49import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
     50import org.openstreetmap.josm.gui.layer.Layer;
     51import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
     52import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
     53import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
     54import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
     55import org.openstreetmap.josm.gui.util.WindowGeometry;
     56import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
     57import org.openstreetmap.josm.io.session.SessionLayerExporter;
     58import org.openstreetmap.josm.io.session.SessionWriter;
     59import org.openstreetmap.josm.spi.preferences.Config;
     60import org.openstreetmap.josm.tools.GBC;
     61import org.openstreetmap.josm.tools.JosmRuntimeException;
     62import org.openstreetmap.josm.tools.Logging;
     63import org.openstreetmap.josm.tools.MultiMap;
     64import org.openstreetmap.josm.tools.Shortcut;
     65import org.openstreetmap.josm.tools.UserCancelException;
     66import org.openstreetmap.josm.tools.Utils;
     67
     68/**
     69 * Saves a JOSM session
     70 * @since xxx
     71 */
     72public class SessionSaveAction extends DiskAccessAction implements MapFrameListener, LayerChangeListener {
     73
     74    private transient List<Layer> layers;
     75    private transient Map<Layer, SessionLayerExporter> exporters;
     76    private transient MultiMap<Layer, Layer> dependencies;
     77
     78    private static final BooleanProperty SAVE_LOCAL_FILES_PROPERTY = new BooleanProperty("session.savelocal", true);
     79    private static final String TOOLTIP_DEFAULT = tr("Save the current session.");
     80
     81    protected FileFilter joz = new ExtensionFileFilter("joz", "joz", tr("Session file (archive) (*.joz)"));
     82    protected FileFilter jos = new ExtensionFileFilter("jos", "jos", tr("Session file (*.jos)"));
     83
     84    private File removeFileOnSuccess;
     85
     86    private static String tooltip = TOOLTIP_DEFAULT;
     87    protected static File sessionFile;
     88    protected static boolean isZipSessionFile;
     89    protected static List<WeakReference<Layer>> layersInSessionFile;
     90
     91    private static final SessionSaveAction instance = new SessionSaveAction();
     92
     93    /**
     94     * Returns the instance
     95     * @return the instance
     96     */
     97    public static final SessionSaveAction getInstance() {
     98        return instance;
     99    }
     100
     101    /**
     102     * Constructs a new {@code SessionSaveAction}.
     103     */
     104    public SessionSaveAction() {
     105        this(true, false);
     106        updateEnabledState();
     107    }
     108
     109    /**
     110     * Constructs a new {@code SessionSaveAction}.
     111     * @param toolbar Register this action for the toolbar preferences?
     112     * @param installAdapters False, if you don't want to install layer changed and selection changed adapters
     113     */
     114    protected SessionSaveAction(boolean toolbar, boolean installAdapters) {
     115        this(tr("Save Session"), "session", TOOLTIP_DEFAULT,
     116                Shortcut.registerShortcut("system:savesession", tr("File: {0}", tr("Save Session...")), KeyEvent.VK_S, Shortcut.ALT_CTRL),
     117                toolbar, "save-session", installAdapters);
     118        setHelpId(ht("/Action/SessionSave"));
     119    }
     120
     121    protected SessionSaveAction(String name, String iconName, String tooltip,
     122            Shortcut shortcut, boolean register, String toolbarId, boolean installAdapters) {
     123
     124        super(name, iconName, tooltip, shortcut, register, toolbarId, installAdapters);
     125        addListeners();
     126    }
     127
     128    @Override
     129    public void actionPerformed(ActionEvent e) {
     130        try {
     131            saveSession(false, false);
     132        } catch (UserCancelException ignore) {
     133            Logging.trace(ignore);
     134        }
     135    }
     136
     137    @Override
     138    public void destroy() {
     139        removeListeners();
     140        super.destroy();
     141    }
     142
     143    /**
     144     * Attempts to save the session.
     145     * @param saveAs true shows the dialog
     146     * @param forceSaveAll saves all layers
     147     * @return if the session and all layers were successfully saved
     148     * @throws UserCancelException when the user has cancelled the save process
     149     */
     150    public boolean saveSession(boolean saveAs, boolean forceSaveAll) throws UserCancelException {
     151        if (!isEnabled()) {
     152            return false;
     153        }
     154
     155        removeFileOnSuccess = null;
     156
     157        SessionSaveAsDialog dlg = new SessionSaveAsDialog();
     158        if (saveAs) {
     159            dlg.showDialog();
     160            if (dlg.getValue() != 1) {
     161                throw new UserCancelException();
     162            }
     163        }
     164
     165        // TODO: resolve dependencies for layers excluded by the user
     166        List<Layer> layersOut = layers.stream()
     167                .filter(layer -> exporters.get(layer) != null && exporters.get(layer).shallExport())
     168                .collect(Collectors.toList());
     169
     170        boolean zipRequired = layersOut.stream().map(l -> exporters.get(l))
     171                .anyMatch(ex -> ex != null && ex.requiresZip());
     172
     173        saveAs = !doGetFile(saveAs, zipRequired);
     174
     175        String fn = sessionFile.getName();
     176
     177        if (!saveAs && layersInSessionFile != null) {
     178            List<String> missingLayers = layersInSessionFile.stream()
     179                    .map(WeakReference::get)
     180                    .filter(Objects::nonNull)
     181                    .filter(l -> !layersOut.contains(l))
     182                    .map(Layer::getName)
     183                    .collect(Collectors.toList());
     184
     185            if (!missingLayers.isEmpty() &&
     186                    !ConditionalOptionPaneUtil.showConfirmationDialog(
     187                            "savesession_layerremoved",
     188                            null,
     189                            new JLabel("<html>"
     190                                    + trn("The following layer has been removed since the session was last saved:",
     191                                          "The following layers have been removed since the session was last saved:", missingLayers.size())
     192                                    + "<ul><li>"
     193                                    + String.join("<li>", missingLayers)
     194                                    + "</ul><br>"
     195                                    + tr("You are about to overwrite the session file \"{0}\". Would you like to proceed?", fn)),
     196                            tr("Layers removed"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
     197                            JOptionPane.OK_OPTION)) {
     198                throw new UserCancelException();
     199            }
     200        }
     201        setCurrentLayers(layersOut);
     202
     203
     204        if (fn.indexOf('.') == -1) {
     205            sessionFile = new File(sessionFile.getPath() + (isZipSessionFile ? ".joz" : ".jos"));
     206            if (!SaveActionBase.confirmOverwrite(sessionFile)) {
     207                throw new UserCancelException();
     208            }
     209        }
     210
     211        Stream<Layer> layersToSaveStream = layersOut.stream()
     212                .filter(layer -> layer.isSavable()
     213                        && layer instanceof AbstractModifiableLayer
     214                        && ((AbstractModifiableLayer) layer).requiresSaveToFile()
     215                        && exporters.get(layer) != null
     216                        && !exporters.get(layer).requiresZip());
     217
     218        boolean success = true;
     219        if (forceSaveAll || SAVE_LOCAL_FILES_PROPERTY.get()) {
     220            // individual files must be saved before the session file as the location may change
     221            if (layersToSaveStream
     222                .map(layer -> SaveAction.getInstance().doSave(layer, true))
     223                .collect(Collectors.toList()) // force evaluation of all elements
     224                .contains(false)) {
     225
     226                new Notification(tr("Not all local files referenced by the session file could be saved."
     227                        + "<br>Make sure you save them before closing JOSM."))
     228                    .setIcon(JOptionPane.WARNING_MESSAGE)
     229                    .setDuration(Notification.TIME_LONG)
     230                    .show();
     231                success = false;
     232            }
     233        } else if (layersToSaveStream.anyMatch(l -> true)) {
     234            new Notification(tr("Not all local files referenced by the session file are saved yet."
     235                    + "<br>Make sure you save them before closing JOSM."))
     236                .setIcon(JOptionPane.INFORMATION_MESSAGE)
     237                .setDuration(Notification.TIME_LONG)
     238                .show();
     239        }
     240
     241        int active = -1;
     242        Layer activeLayer = getLayerManager().getActiveLayer();
     243        if (activeLayer != null) {
     244            active = layersOut.indexOf(activeLayer);
     245        }
     246
     247        SessionWriter sw = new SessionWriter(layersOut, active, exporters, dependencies, isZipSessionFile);
     248        try {
     249            Notification savingNotification = showSavingNotification(sessionFile.getName());
     250            sw.write(sessionFile);
     251            SaveActionBase.addToFileOpenHistory(sessionFile);
     252            if (removeFileOnSuccess != null) {
     253                PreferencesUtils.removeFromList(Config.getPref(), "file-open.history", removeFileOnSuccess.getCanonicalPath());
     254                removeFileOnSuccess.delete();
     255                removeFileOnSuccess = null;
     256            }
     257            showSavedNotification(savingNotification, sessionFile.getName());
     258        } catch (IOException ex) {
     259            Logging.error(ex);
     260            HelpAwareOptionPane.showMessageDialogInEDT(
     261                    MainApplication.getMainFrame(),
     262                    tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>",
     263                            sessionFile.getName(), Utils.escapeReservedCharactersHTML(ex.getMessage())),
     264                    tr("IO Error"),
     265                    JOptionPane.ERROR_MESSAGE,
     266                    null
     267            );
     268            success = false;
     269        }
     270        return success;
     271    }
     272
     273    /**
     274     * Sets the current session file. Asks the user if necessary
     275     * @param saveAs alwas ask the user
     276     * @param zipRequired zip
     277     * @return if the user was asked
     278     * @throws UserCancelException when the user has cancelled the save process
     279     */
     280    protected boolean doGetFile(boolean saveAs, boolean zipRequired) throws UserCancelException {
     281        if (!saveAs && sessionFile != null) {
     282
     283            if (isZipSessionFile || !zipRequired)
     284                return true;
     285
     286            Logging.info("Converting *.jos to *.joz because a new layer has been added that requires zip format");
     287            String oldPath = sessionFile.getAbsolutePath();
     288            int i = oldPath.lastIndexOf('.');
     289            File jozFile = new File(i < 0 ? oldPath : oldPath.substring(0, i) + ".joz");
     290            if (!jozFile.exists()) {
     291                removeFileOnSuccess = sessionFile;
     292                setCurrentSession(jozFile, true);
     293                return true;
     294            }
     295            Logging.warn("Asking user to choose a new location for the *.joz file because it already exists");
     296        }
     297
     298        doGetFileChooser(zipRequired);
     299        return false;
     300    }
     301
     302    protected void doGetFileChooser(boolean zipRequired) throws UserCancelException {
     303        AbstractFileChooser fc;
     304
     305        if (zipRequired) {
     306            fc = createAndOpenFileChooser(false, false, tr("Save Session"), joz, JFileChooser.FILES_ONLY, "lastDirectory");
     307        } else {
     308            fc = createAndOpenFileChooser(false, false, tr("Save Session"), Arrays.asList(jos, joz), jos,
     309                    JFileChooser.FILES_ONLY, "lastDirectory");
     310        }
     311
     312        if (fc == null) {
     313            throw new UserCancelException();
     314        }
     315
     316        File f = fc.getSelectedFile();
     317        FileFilter ff = fc.getFileFilter();
     318        boolean zip;
     319
     320        if (zipRequired || joz.equals(ff)) {
     321            zip = true;
     322        } else if (jos.equals(ff)) {
     323            zip = false;
     324        } else {
     325            zip = Utils.hasExtension(f.getName(), "joz");
     326        }
     327        setCurrentSession(f, zip);
     328    }
     329
     330    /**
     331     * The "Save Session" dialog
     332     */
     333    public class SessionSaveAsDialog extends ExtendedDialog {
     334
     335        /**
     336         * Constructs a new {@code SessionSaveAsDialog}.
     337         */
     338        public SessionSaveAsDialog() {
     339            super(MainApplication.getMainFrame(), tr("Save Session"), tr("Save As"), tr("Cancel"));
     340            configureContextsensitiveHelp("Action/SessionSaveAs", true /* show help button */);
     341            initialize();
     342            setButtonIcons("save_as", "cancel");
     343            setDefaultButton(1);
     344            setRememberWindowGeometry(getClass().getName() + ".geometry",
     345                    WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(450, 450)));
     346            setContent(build(), false);
     347        }
     348
     349        /**
     350         * Initializes action.
     351         */
     352        public final void initialize() {
     353            layers = new ArrayList<>(getLayerManager().getLayers());
     354            exporters = new HashMap<>();
     355            dependencies = new MultiMap<>();
     356
     357            Set<Layer> noExporter = new HashSet<>();
     358
     359            for (Layer layer : layers) {
     360                SessionLayerExporter exporter = null;
     361                try {
     362                    exporter = SessionWriter.getSessionLayerExporter(layer);
     363                } catch (IllegalArgumentException | JosmRuntimeException e) {
     364                    Logging.error(e);
     365                }
     366                if (exporter != null) {
     367                    exporters.put(layer, exporter);
     368                    Collection<Layer> deps = exporter.getDependencies();
     369                    if (deps != null) {
     370                        dependencies.putAll(layer, deps);
     371                    } else {
     372                        dependencies.putVoid(layer);
     373                    }
     374                } else {
     375                    noExporter.add(layer);
     376                    exporters.put(layer, null);
     377                }
     378            }
     379
     380            int numNoExporter = 0;
     381            WHILE: while (numNoExporter != noExporter.size()) {
     382                numNoExporter = noExporter.size();
     383                for (Layer layer : layers) {
     384                    if (noExporter.contains(layer)) continue;
     385                    for (Layer depLayer : dependencies.get(layer)) {
     386                        if (noExporter.contains(depLayer)) {
     387                            noExporter.add(layer);
     388                            exporters.put(layer, null);
     389                            break WHILE;
     390                        }
     391                    }
     392                }
     393            }
     394        }
     395
     396        protected final Component build() {
     397            JPanel op = new JPanel(new GridBagLayout());
     398            JPanel ip = new JPanel(new GridBagLayout());
     399            for (Layer layer : layers) {
     400                Component exportPanel;
     401                SessionLayerExporter exporter = exporters.get(layer);
     402                if (exporter == null) {
     403                    if (!exporters.containsKey(layer)) throw new AssertionError();
     404                    exportPanel = getDisabledExportPanel(layer);
     405                } else {
     406                    exportPanel = exporter.getExportPanel();
     407                }
     408                if (exportPanel == null) continue;
     409                JPanel wrapper = new JPanel(new GridBagLayout());
     410                wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
     411                wrapper.add(exportPanel, GBC.std().fill(GBC.HORIZONTAL));
     412                ip.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 2, 4, 2));
     413            }
     414            ip.add(GBC.glue(0, 1), GBC.eol().fill(GBC.VERTICAL));
     415            JScrollPane sp = new JScrollPane(ip);
     416            sp.setBorder(BorderFactory.createEmptyBorder());
     417            JPanel p = new JPanel(new GridBagLayout());
     418            p.add(sp, GBC.eol().fill());
     419            final JTabbedPane tabs = new JTabbedPane();
     420            tabs.addTab(tr("Layers"), p);
     421            op.add(tabs, GBC.eol().fill());
     422            JCheckBox chkSaveLocal = new JCheckBox(tr("Save all local files to disk"), SAVE_LOCAL_FILES_PROPERTY.get());
     423            chkSaveLocal.addChangeListener(l -> {
     424                SAVE_LOCAL_FILES_PROPERTY.put(chkSaveLocal.isSelected());
     425            });
     426            op.add(chkSaveLocal);
     427            return op;
     428        }
     429
     430        protected final Component getDisabledExportPanel(Layer layer) {
     431            JPanel p = new JPanel(new GridBagLayout());
     432            JCheckBox include = new JCheckBox();
     433            include.setEnabled(false);
     434            JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEADING);
     435            lbl.setToolTipText(tr("No exporter for this layer"));
     436            lbl.setLabelFor(include);
     437            lbl.setEnabled(false);
     438            p.add(include, GBC.std());
     439            p.add(lbl, GBC.std());
     440            p.add(GBC.glue(1, 0), GBC.std().fill(GBC.HORIZONTAL));
     441            return p;
     442        }
     443    }
     444
     445    protected void addListeners() {
     446        MainApplication.addMapFrameListener(this);
     447        MainApplication.getLayerManager().addLayerChangeListener(this);
     448    }
     449
     450    protected void removeListeners() {
     451        MainApplication.removeMapFrameListener(this);
     452        MainApplication.getLayerManager().removeLayerChangeListener(this);
     453    }
     454
     455    @Override
     456    protected void updateEnabledState() {
     457        setEnabled(MainApplication.isDisplayingMapView());
     458    }
     459
     460    @Override
     461    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
     462        updateEnabledState();
     463    }
     464
     465    @Override
     466    public void layerAdded(LayerAddEvent e) {
     467        // not used
     468    }
     469
     470    @Override
     471    public void layerRemoving(LayerRemoveEvent e) {
     472        if (e.isLastLayer()) {
     473            setCurrentSession(null, false);
     474        }
     475    }
     476
     477    @Override
     478    public void layerOrderChanged(LayerOrderChangeEvent e) {
     479        // not used
     480    }
     481
     482    /**
     483     * Sets the current session file and the layers included in that file
     484     * @param file file
     485     * @param zip if it is a zip session file
     486     * @param layers layers that are currently represented in the session file
     487     */
     488    public static void setCurrentSession(File file, boolean zip, List<Layer> layers) {
     489        setCurrentLayers(layers);
     490        setCurrentSession(file, zip);
     491    }
     492
     493    /**
     494     * Sets the current session file
     495     * @param file file
     496     * @param zip if it is a zip session file
     497     */
     498    public static void setCurrentSession(File file, boolean zip) {
     499        sessionFile = file;
     500        isZipSessionFile = zip;
     501        if (file == null) {
     502            tooltip = TOOLTIP_DEFAULT;
     503        } else {
     504            tooltip = tr("Save the current session file \"{0}\".", file.getName());
     505        }
     506        getInstance().setTooltip(tooltip);
     507    }
     508
     509    /**
     510     * Sets the layers that are currently represented in the session file
     511     * @param layers layers
     512     */
     513    public static void setCurrentLayers(List<Layer> layers) {
     514        layersInSessionFile = layers.stream()
     515                .filter(l -> l instanceof AbstractModifiableLayer)
     516                .map(WeakReference::new)
     517                .collect(Collectors.toList());
     518    }
     519
     520    /**
     521     * Returns the tooltip for the component
     522     * @return the tooltip for the component
     523     */
     524    public static String getTooltip() {
     525        return tooltip;
     526    }
     527
     528}
  • src/org/openstreetmap/josm/actions/SessionSaveAsAction.java

     
    44import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66
    7 import java.awt.Component;
    8 import java.awt.Dimension;
    9 import java.awt.GridBagLayout;
    107import java.awt.event.ActionEvent;
    11 import java.io.File;
    12 import java.io.IOException;
    13 import java.util.ArrayList;
    14 import java.util.Arrays;
    15 import java.util.Collection;
    16 import java.util.HashMap;
    17 import java.util.HashSet;
    18 import java.util.List;
    19 import java.util.Map;
    20 import java.util.Set;
    21 import java.util.stream.Collectors;
    22 import java.util.stream.Stream;
     8import java.awt.event.KeyEvent;
    239
    24 import javax.swing.BorderFactory;
    25 import javax.swing.JCheckBox;
    26 import javax.swing.JFileChooser;
    27 import javax.swing.JLabel;
    28 import javax.swing.JOptionPane;
    29 import javax.swing.JPanel;
    30 import javax.swing.JScrollPane;
    31 import javax.swing.JTabbedPane;
    32 import javax.swing.SwingConstants;
    33 import javax.swing.border.EtchedBorder;
    34 import javax.swing.filechooser.FileFilter;
    35 
    36 import org.openstreetmap.josm.data.preferences.BooleanProperty;
    37 import org.openstreetmap.josm.gui.ExtendedDialog;
    38 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
    3910import org.openstreetmap.josm.gui.MainApplication;
    40 import org.openstreetmap.josm.gui.MapFrame;
    41 import org.openstreetmap.josm.gui.MapFrameListener;
    42 import org.openstreetmap.josm.gui.Notification;
    43 import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
    44 import org.openstreetmap.josm.gui.layer.Layer;
    45 import org.openstreetmap.josm.gui.util.WindowGeometry;
    46 import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
    47 import org.openstreetmap.josm.io.session.SessionLayerExporter;
    48 import org.openstreetmap.josm.io.session.SessionWriter;
    49 import org.openstreetmap.josm.tools.GBC;
    50 import org.openstreetmap.josm.tools.JosmRuntimeException;
    5111import org.openstreetmap.josm.tools.Logging;
    52 import org.openstreetmap.josm.tools.MultiMap;
     12import org.openstreetmap.josm.tools.Shortcut;
    5313import org.openstreetmap.josm.tools.UserCancelException;
    54 import org.openstreetmap.josm.tools.Utils;
    5514
    5615/**
    57  * Saves a JOSM session
     16 * Saves a JOSM session to a new file
    5817 * @since 4685
    5918 */
    60 public class SessionSaveAsAction extends DiskAccessAction implements MapFrameListener {
     19public class SessionSaveAsAction extends SessionSaveAction {
    6120
    62     private transient List<Layer> layers;
    63     private transient Map<Layer, SessionLayerExporter> exporters;
    64     private transient MultiMap<Layer, Layer> dependencies;
    65 
    66     private static final BooleanProperty SAVE_LOCAL_FILES_PROPERTY = new BooleanProperty("session.savelocal", true);
    67 
    6821    /**
    6922     * Constructs a new {@code SessionSaveAsAction}.
    7023     */
     
    7932     * @param installAdapters False, if you don't want to install layer changed and selection changed adapters
    8033     */
    8134    protected SessionSaveAsAction(boolean toolbar, boolean installAdapters) {
     35
    8236        super(tr("Save Session As..."), "session", tr("Save the current session to a new file."),
    83                 null, toolbar, "save_as-session", installAdapters);
     37                Shortcut.registerShortcut("system:savesessionas", tr("File: {0}", tr("Save Session As...")),
     38                        KeyEvent.VK_S, Shortcut.ALT_CTRL_SHIFT),
     39                toolbar, "save_as-session", installAdapters);
     40
    8441        setHelpId(ht("/Action/SessionSaveAs"));
    85         MainApplication.addMapFrameListener(this);
    8642    }
    8743
    8844    @Override
    8945    public void actionPerformed(ActionEvent e) {
    9046        try {
    91             saveSession();
     47            saveSession(true, false);
    9248        } catch (UserCancelException ignore) {
    9349            Logging.trace(ignore);
    9450        }
     
    9551    }
    9652
    9753    @Override
    98     public void destroy() {
    99         MainApplication.removeMapFrameListener(this);
    100         super.destroy();
     54    protected void addListeners() {
     55        MainApplication.addMapFrameListener(this);
    10156    }
    10257
    103     /**
    104      * Attempts to save the session.
    105      * @throws UserCancelException when the user has cancelled the save process.
    106      * @since 8913
    107      */
    108     public void saveSession() throws UserCancelException {
    109         if (!isEnabled()) {
    110             return;
    111         }
    112 
    113         SessionSaveAsDialog dlg = new SessionSaveAsDialog();
    114         dlg.showDialog();
    115         if (dlg.getValue() != 1) {
    116             throw new UserCancelException();
    117         }
    118 
    119         // TODO: resolve dependencies for layers excluded by the user
    120         List<Layer> layersOut = layers.stream()
    121                 .filter(layer -> exporters.get(layer) != null && exporters.get(layer).shallExport())
    122                 .collect(Collectors.toList());
    123 
    124         boolean zipRequired = layersOut.stream().map(exporters::get)
    125                 .anyMatch(ex -> ex != null && ex.requiresZip());
    126 
    127         FileFilter joz = new ExtensionFileFilter("joz", "joz", tr("Session file (archive) (*.joz)"));
    128         FileFilter jos = new ExtensionFileFilter("jos", "jos", tr("Session file (*.jos)"));
    129 
    130         AbstractFileChooser fc;
    131 
    132         if (zipRequired) {
    133             fc = createAndOpenFileChooser(false, false, tr("Save Session"), joz, JFileChooser.FILES_ONLY, "lastDirectory");
    134         } else {
    135             fc = createAndOpenFileChooser(false, false, tr("Save Session"), Arrays.asList(jos, joz), jos,
    136                     JFileChooser.FILES_ONLY, "lastDirectory");
    137         }
    138 
    139         if (fc == null) {
    140             throw new UserCancelException();
    141         }
    142 
    143         File file = fc.getSelectedFile();
    144         String fn = file.getName();
    145 
    146         boolean zip;
    147         FileFilter ff = fc.getFileFilter();
    148         if (zipRequired || joz.equals(ff)) {
    149             zip = true;
    150         } else if (jos.equals(ff)) {
    151             zip = false;
    152         } else {
    153             if (Utils.hasExtension(fn, "joz")) {
    154                 zip = true;
    155             } else {
    156                 zip = false;
    157             }
    158         }
    159         if (fn.indexOf('.') == -1) {
    160             file = new File(file.getPath() + (zip ? ".joz" : ".jos"));
    161             if (!SaveActionBase.confirmOverwrite(file)) {
    162                 throw new UserCancelException();
    163             }
    164         }
    165 
    166         Stream<Layer> layersToSaveStream = layersOut.stream()
    167                 .filter(layer -> layer.isSavable()
    168                         && layer instanceof AbstractModifiableLayer
    169                         && ((AbstractModifiableLayer) layer).requiresSaveToFile()
    170                         && exporters.get(layer) != null
    171                         && !exporters.get(layer).requiresZip());
    172 
    173         if (SAVE_LOCAL_FILES_PROPERTY.get()) {
    174             // individual files must be saved before the session file as the location may change
    175             if (layersToSaveStream
    176                 .map(layer -> SaveAction.getInstance().doSave(layer, true))
    177                 .collect(Collectors.toList()) // force evaluation of all elements
    178                 .contains(false)) {
    179 
    180                 new Notification(tr("Not all local files referenced by the session file could be saved."
    181                         + "<br>Make sure you save them before closing JOSM."))
    182                     .setIcon(JOptionPane.WARNING_MESSAGE)
    183                     .setDuration(Notification.TIME_LONG)
    184                     .show();
    185             }
    186         } else if (layersToSaveStream.anyMatch(l -> true)) {
    187             new Notification(tr("Not all local files referenced by the session file are saved yet."
    188                     + "<br>Make sure you save them before closing JOSM."))
    189                 .setIcon(JOptionPane.INFORMATION_MESSAGE)
    190                 .setDuration(Notification.TIME_LONG)
    191                 .show();
    192         }
    193 
    194         int active = -1;
    195         Layer activeLayer = getLayerManager().getActiveLayer();
    196         if (activeLayer != null) {
    197             active = layersOut.indexOf(activeLayer);
    198         }
    199 
    200         SessionWriter sw = new SessionWriter(layersOut, active, exporters, dependencies, zip);
    201         try {
    202             Notification savingNotification = showSavingNotification(file.getName());
    203             sw.write(file);
    204             SaveActionBase.addToFileOpenHistory(file);
    205             showSavedNotification(savingNotification, file.getName());
    206         } catch (IOException ex) {
    207             Logging.error(ex);
    208             HelpAwareOptionPane.showMessageDialogInEDT(
    209                     MainApplication.getMainFrame(),
    210                     tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>",
    211                             file.getName(), Utils.escapeReservedCharactersHTML(ex.getMessage())),
    212                     tr("IO Error"),
    213                     JOptionPane.ERROR_MESSAGE,
    214                     null
    215             );
    216         }
    217     }
    218 
    219     /**
    220      * The "Save Session" dialog
    221      */
    222     public class SessionSaveAsDialog extends ExtendedDialog {
    223 
    224         /**
    225          * Constructs a new {@code SessionSaveAsDialog}.
    226          */
    227         public SessionSaveAsDialog() {
    228             super(MainApplication.getMainFrame(), tr("Save Session"), tr("Save As"), tr("Cancel"));
    229             configureContextsensitiveHelp("Action/SessionSaveAs", true /* show help button */);
    230             initialize();
    231             setButtonIcons("save_as", "cancel");
    232             setDefaultButton(1);
    233             setRememberWindowGeometry(getClass().getName() + ".geometry",
    234                     WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(450, 450)));
    235             setContent(build(), false);
    236         }
    237 
    238         /**
    239          * Initializes action.
    240          */
    241         public final void initialize() {
    242             layers = new ArrayList<>(getLayerManager().getLayers());
    243             exporters = new HashMap<>();
    244             dependencies = new MultiMap<>();
    245 
    246             Set<Layer> noExporter = new HashSet<>();
    247 
    248             for (Layer layer : layers) {
    249                 SessionLayerExporter exporter = null;
    250                 try {
    251                     exporter = SessionWriter.getSessionLayerExporter(layer);
    252                 } catch (IllegalArgumentException | JosmRuntimeException e) {
    253                     Logging.error(e);
    254                 }
    255                 if (exporter != null) {
    256                     exporters.put(layer, exporter);
    257                     Collection<Layer> deps = exporter.getDependencies();
    258                     if (deps != null) {
    259                         dependencies.putAll(layer, deps);
    260                     } else {
    261                         dependencies.putVoid(layer);
    262                     }
    263                 } else {
    264                     noExporter.add(layer);
    265                     exporters.put(layer, null);
    266                 }
    267             }
    268 
    269             int numNoExporter = 0;
    270             WHILE: while (numNoExporter != noExporter.size()) {
    271                 numNoExporter = noExporter.size();
    272                 for (Layer layer : layers) {
    273                     if (noExporter.contains(layer)) continue;
    274                     for (Layer depLayer : dependencies.get(layer)) {
    275                         if (noExporter.contains(depLayer)) {
    276                             noExporter.add(layer);
    277                             exporters.put(layer, null);
    278                             break WHILE;
    279                         }
    280                     }
    281                 }
    282             }
    283         }
    284 
    285         protected final Component build() {
    286             JPanel op = new JPanel(new GridBagLayout());
    287             JPanel ip = new JPanel(new GridBagLayout());
    288             for (Layer layer : layers) {
    289                 JPanel wrapper = new JPanel(new GridBagLayout());
    290                 wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
    291                 Component exportPanel;
    292                 SessionLayerExporter exporter = exporters.get(layer);
    293                 if (exporter == null) {
    294                     if (!exporters.containsKey(layer)) throw new AssertionError();
    295                     exportPanel = getDisabledExportPanel(layer);
    296                 } else {
    297                     exportPanel = exporter.getExportPanel();
    298                 }
    299                 wrapper.add(exportPanel, GBC.std().fill(GBC.HORIZONTAL));
    300                 ip.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 2, 4, 2));
    301             }
    302             ip.add(GBC.glue(0, 1), GBC.eol().fill(GBC.VERTICAL));
    303             JScrollPane sp = new JScrollPane(ip);
    304             sp.setBorder(BorderFactory.createEmptyBorder());
    305             JPanel p = new JPanel(new GridBagLayout());
    306             p.add(sp, GBC.eol().fill());
    307             final JTabbedPane tabs = new JTabbedPane();
    308             tabs.addTab(tr("Layers"), p);
    309             op.add(tabs, GBC.eol().fill());
    310             JCheckBox chkSaveLocal = new JCheckBox(tr("Save all local files to disk"), SAVE_LOCAL_FILES_PROPERTY.get());
    311             chkSaveLocal.addChangeListener(l -> {
    312                 SAVE_LOCAL_FILES_PROPERTY.put(chkSaveLocal.isSelected());
    313             });
    314             op.add(chkSaveLocal);
    315             return op;
    316         }
    317 
    318         protected final Component getDisabledExportPanel(Layer layer) {
    319             JPanel p = new JPanel(new GridBagLayout());
    320             JCheckBox include = new JCheckBox();
    321             include.setEnabled(false);
    322             JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEADING);
    323             lbl.setToolTipText(tr("No exporter for this layer"));
    324             lbl.setLabelFor(include);
    325             lbl.setEnabled(false);
    326             p.add(include, GBC.std());
    327             p.add(lbl, GBC.std());
    328             p.add(GBC.glue(1, 0), GBC.std().fill(GBC.HORIZONTAL));
    329             return p;
    330         }
    331     }
    332 
    33358    @Override
    334     protected void updateEnabledState() {
    335         setEnabled(MainApplication.isDisplayingMapView());
     59    protected void removeListeners() {
     60        MainApplication.removeMapFrameListener(this);
    33661    }
    337 
    338     @Override
    339     public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
    340         updateEnabledState();
    341     }
    34262}
  • src/org/openstreetmap/josm/gui/MainMenu.java

     
    9696import org.openstreetmap.josm.actions.SearchNotesDownloadAction;
    9797import org.openstreetmap.josm.actions.SelectAllAction;
    9898import org.openstreetmap.josm.actions.SelectNonBranchingWaySequencesAction;
     99import org.openstreetmap.josm.actions.SessionSaveAction;
    99100import org.openstreetmap.josm.actions.SessionSaveAsAction;
    100101import org.openstreetmap.josm.actions.ShowStatusReportAction;
    101102import org.openstreetmap.josm.actions.SimplifyWayAction;
     
    176177    public final SaveAction save = SaveAction.getInstance();
    177178    /** File / Save As... **/
    178179    public final SaveAsAction saveAs = SaveAsAction.getInstance();
     180    /** File / Session &gt; Save Session **/
     181    public SessionSaveAction sessionSave = SessionSaveAction.getInstance();
    179182    /** File / Session &gt; Save Session As... **/
    180     public SessionSaveAsAction sessionSaveAs;
     183    public SessionSaveAsAction sessionSaveAs = new SessionSaveAsAction();
    181184    /** File / Export to GPX... **/
    182185    public final GpxExportAction gpxExport = new GpxExportAction();
    183186    /** File / Download from OSM... **/
     
    738741        fileMenu.addSeparator();
    739742        add(fileMenu, save);
    740743        add(fileMenu, saveAs);
    741         sessionSaveAs = new SessionSaveAsAction();
    742         ExpertToggleAction.addVisibilitySwitcher(fileMenu.add(sessionSaveAs));
     744        add(fileMenu, sessionSave, true);
     745        add(fileMenu, sessionSaveAs, true);
    743746        add(fileMenu, gpxExport, true);
    744747        fileMenu.addSeparator();
    745748        add(fileMenu, download);
  • src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java

     
    4040import javax.swing.event.TableModelEvent;
    4141import javax.swing.event.TableModelListener;
    4242
    43 import org.openstreetmap.josm.actions.SessionSaveAsAction;
     43import org.openstreetmap.josm.actions.JosmAction;
     44import org.openstreetmap.josm.actions.SessionSaveAction;
    4445import org.openstreetmap.josm.actions.UploadAction;
    4546import org.openstreetmap.josm.gui.ExceptionDialogUtil;
    4647import org.openstreetmap.josm.gui.MainApplication;
     
    9293    private final UploadAndSaveProgressRenderer pnlUploadLayers = new UploadAndSaveProgressRenderer();
    9394
    9495    private final SaveAndProceedAction saveAndProceedAction = new SaveAndProceedAction();
    95     private final SaveSessionAction saveSessionAction = new SaveSessionAction();
     96    private final SaveSessionButtonAction saveSessionAction = new SaveSessionButtonAction();
    9697    private final DiscardAndProceedAction discardAndProceedAction = new DiscardAndProceedAction();
    9798    private final CancelAction cancelAction = new CancelAction();
    9899    private transient SaveAndUploadTask saveAndUploadTask;
     
    432433        }
    433434    }
    434435
    435     class SaveSessionAction extends SessionSaveAsAction {
     436    class SaveSessionButtonAction extends JosmAction {
    436437
    437         SaveSessionAction() {
    438             super(false, false);
     438        SaveSessionButtonAction() {
     439            super(tr("Save Session"), "session", SessionSaveAction.getTooltip(), null, false, null, false);
    439440        }
    440441
    441442        @Override
    442443        public void actionPerformed(ActionEvent e) {
    443444            try {
    444                 saveSession();
    445                 setUserAction(UserAction.PROCEED);
    446                 closeDialog();
     445                if (SessionSaveAction.getInstance().saveSession(false, true)) {
     446                    setUserAction(UserAction.PROCEED);
     447                    closeDialog();
     448                }
    447449            } catch (UserCancelException ignore) {
    448450                Logging.trace(ignore);
    449451            }
  • src/org/openstreetmap/josm/gui/layer/LayerManager.java

     
    131131        LayerRemoveEvent(LayerManager source, Layer removedLayer) {
    132132            super(source);
    133133            this.removedLayer = removedLayer;
    134             this.lastLayer = source.getLayers().size() == 1;
     134            this.lastLayer = source.getLayers().isEmpty();
    135135        }
    136136
    137137        /**
  • src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java

     
    758758     * @return GPX data
    759759     */
    760760    public static GpxData toGpxData(DataSet data, File file) {
    761         GpxData gpxData = new GpxData();
     761        GpxData gpxData = new GpxData(true);
    762762        fillGpxData(gpxData, data, file, GpxConstants.GPX_PREFIX);
     763        gpxData.endUpdate();
    763764        return gpxData;
    764765    }
    765766
     
    10101011
    10111012        @Override
    10121013        public void actionPerformed(ActionEvent e) {
     1014            String name = getName().replaceAll("^" + tr("Converted from: {0}", ""), "");
    10131015            final GpxData gpxData = toGpxData();
    1014             final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", getName()));
     1016            final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", name), true);
    10151017            if (getAssociatedFile() != null) {
    10161018                String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
    10171019                gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
     1020                gpxLayer.getGpxData().setModified(true);
    10181021            }
    10191022            MainApplication.getLayerManager().addLayer(gpxLayer, false);
    10201023            if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
  • src/org/openstreetmap/josm/gui/layer/gpx/ConvertToDataLayerAction.java

     
    7171            if (err > 0) {
    7272                SimplifyWayAction.simplifyWays(ways, err);
    7373            }
    74             final OsmDataLayer osmLayer = new OsmDataLayer(ds, tr("Converted from: {0}", layer.getName()), null);
     74            String name = layer.getName().replaceAll("^" + tr("Converted from: {0}", ""), "");
     75            final OsmDataLayer osmLayer = new OsmDataLayer(ds, tr("Converted from: {0}", name), null);
    7576            if (layer.getAssociatedFile() != null) {
    7677                osmLayer.setAssociatedFile(new File(layer.getAssociatedFile().getParentFile(),
    7778                        layer.getAssociatedFile().getName() + ".osm"));
  • src/org/openstreetmap/josm/io/session/GenericSessionExporter.java

     
    191191            String zipPath = "layers/" + String.format("%02d", support.getLayerIndex()) + "/data." + extension;
    192192            file.appendChild(support.createTextNode(zipPath));
    193193            addDataFile(support.getOutputStreamZip(zipPath));
     194            layer.setAssociatedFile(null);
     195            if (layer instanceof AbstractModifiableLayer) {
     196                ((AbstractModifiableLayer) layer).onPostSaveToFile();
     197            }
    194198        } else {
    195199            try {
    196200                File f = layer.getAssociatedFile();
  • src/org/openstreetmap/josm/io/session/GpxTracksSessionExporter.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.io.session;
    33
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.io.IOException;
    47import java.io.OutputStream;
    58import java.io.OutputStreamWriter;
    69import java.io.PrintWriter;
     
    811import java.nio.charset.StandardCharsets;
    912import java.time.Instant;
    1013
     14import javax.swing.JCheckBox;
     15import javax.swing.JPanel;
     16
    1117import org.openstreetmap.josm.gui.layer.GpxLayer;
    1218import org.openstreetmap.josm.io.GpxWriter;
     19import org.openstreetmap.josm.io.session.SessionWriter.ExportSupport;
     20import org.openstreetmap.josm.tools.GBC;
     21import org.w3c.dom.Element;
    1322
    1423/**
    1524 * Session exporter for {@link GpxLayer}.
     
    1827public class GpxTracksSessionExporter extends GenericSessionExporter<GpxLayer> {
    1928
    2029    private Instant metaTime;
     30    private JCheckBox chkMarkers;
     31    private boolean hasMarkerLayer;
    2132
    2233    /**
    2334     * Constructs a new {@code GpxTracksSessionExporter}.
     
    3243        if (layer.data == null) {
    3344            throw new IllegalArgumentException("GPX layer without data: " + layer);
    3445        }
     46
     47        hasMarkerLayer = layer.getLinkedMarkerLayer() != null
     48                && layer.getLinkedMarkerLayer().data != null
     49                && !layer.getLinkedMarkerLayer().data.isEmpty();
    3550    }
    3651
    3752    @Override
     53    public JPanel getExportPanel() {
     54        JPanel p = super.getExportPanel();
     55        if (hasMarkerLayer) {
     56            chkMarkers = new JCheckBox();
     57            chkMarkers.setText(tr("include marker layer \"{0}\"", layer.getLinkedMarkerLayer().getName()));
     58            chkMarkers.setSelected(true);
     59            p.add(chkMarkers, GBC.eol().insets(12, 0, 0, 5));
     60        }
     61        return p;
     62    }
     63
     64    @Override
     65    public Element export(ExportSupport support) throws IOException {
     66        Element el = super.export(support);
     67        if (hasMarkerLayer && (chkMarkers == null || chkMarkers.isSelected())) {
     68            Element markerEl = support.createElement("markerLayer");
     69            markerEl.setAttribute("index", Integer.toString(support.getLayerIndexOf(layer.getLinkedMarkerLayer())));
     70            markerEl.setAttribute("name", layer.getLinkedMarkerLayer().getName());
     71            markerEl.setAttribute("visible", Boolean.toString(layer.getLinkedMarkerLayer().isVisible()));
     72            if (layer.getLinkedMarkerLayer().getOpacity() != 1) {
     73                markerEl.setAttribute("opacity", Double.toString(layer.getLinkedMarkerLayer().getOpacity()));
     74            }
     75            el.appendChild(markerEl);
     76        }
     77        return el;
     78    }
     79
     80    @Override
    3881    @SuppressWarnings("resource")
    3982    protected void addDataFile(OutputStream out) {
    4083        Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
  • src/org/openstreetmap/josm/io/session/GpxTracksSessionImporter.java

     
    1919import org.openstreetmap.josm.gui.layer.Layer;
    2020import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    2121import org.openstreetmap.josm.io.IllegalDataException;
     22import org.openstreetmap.josm.tools.Logging;
    2223import org.openstreetmap.josm.tools.Utils;
    2324import org.w3c.dom.Element;
     25import org.w3c.dom.Node;
     26import org.w3c.dom.NodeList;
    2427
    2528/**
    2629 * Session exporter for {@link GpxLayer}.
     
    5760                if (importData.getGpxLayer() != null && importData.getGpxLayer().data != null) {
    5861                    importData.getGpxLayer().data.fromSession = true;
    5962                }
     63                NodeList markerNodes = elem.getElementsByTagName("markerLayer");
     64                if (markerNodes.getLength() > 0 && markerNodes.item(0).getNodeType() == Node.ELEMENT_NODE) {
     65                    Element markerEl = (Element) markerNodes.item(0);
     66                    try {
     67                        int index = Integer.parseInt(markerEl.getAttribute("index"));
     68                        support.addSubLayer(index, importData.getMarkerLayer(), markerEl);
     69                    } catch (NumberFormatException ex) {
     70                        Logging.warn(ex);
     71                    }
     72                }
    6073
    6174                support.addPostLayersTask(importData.getPostLayerTask());
    6275                return getLayer(importData);
  • src/org/openstreetmap/josm/io/session/MarkerSessionExporter.java

     
    3434public class MarkerSessionExporter extends AbstractSessionExporter<MarkerLayer> {
    3535
    3636    private Instant metaTime;
     37    private boolean canExport = true;
    3738
    3839    /**
    3940     * Constructs a new {@code MarkerSessionExporter}.
     
    5354
    5455    @Override
    5556    public Component getExportPanel() {
     57        export.setSelected(true); //true even when not shown to the user as the index should be reserved for the corresponding GPX layer
     58        if (layer.fromLayer != null && layer.fromLayer.getData() != null) {
     59            canExport = false;
     60            return null;
     61        }
    5662        final JPanel p = new JPanel(new GridBagLayout());
    57         export.setSelected(true);
    5863        final JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEADING);
    5964        lbl.setToolTipText(layer.getToolTipText());
    6065        lbl.setLabelFor(export);
     
    6671
    6772    @Override
    6873    public boolean requiresZip() {
    69         return true;
     74        return canExport;
    7075    }
    7176
    7277    @Override
    7378    public Element export(ExportSupport support) throws IOException {
     79        if (!canExport) return null;
     80
    7481        Element layerEl = support.createElement("layer");
    7582        layerEl.setAttribute("type", "markers");
    7683        layerEl.setAttribute("version", "0.1");
  • src/org/openstreetmap/josm/io/session/MarkerSessionImporter.java

     
    4848
    4949                support.addPostLayersTask(importData.getPostLayerTask());
    5050
     51                importData.getGpxLayer().destroy();
    5152                return importData.getMarkerLayer();
    5253            }
    5354        } catch (XPathExpressionException e) {
  • src/org/openstreetmap/josm/io/session/SessionReader.java

     
    1414import java.net.URISyntaxException;
    1515import java.nio.charset.StandardCharsets;
    1616import java.nio.file.Files;
     17import java.util.AbstractMap.SimpleEntry;
    1718import java.util.ArrayList;
    1819import java.util.Collection;
    1920import java.util.Collections;
     
    250251        private final String layerName;
    251252        private final int layerIndex;
    252253        private final List<LayerDependency> layerDependencies;
     254        private Map<Integer, Entry<Layer, Element>> subLayers;
    253255
    254256        /**
    255257         * Path of the file inside the zip archive.
     
    279281        }
    280282
    281283        /**
     284         * Add sub layers
     285         * @param idx index
     286         * @param layer sub layer
     287         * @param el The XML element of the sub layer.
     288         *           Should contain "index" and "name" attributes.
     289         *           Can contain "opacity" and "visible" attributes
     290         * @since xxx
     291         */
     292        public void addSubLayer(int idx, Layer layer, Element el) {
     293            if (subLayers == null) {
     294                subLayers = new HashMap<>();
     295            }
     296            subLayers.put(idx, new SimpleEntry<>(layer, el));
     297        }
     298
     299        /**
     300         * Returns the sub layers
     301         * @return the sub layers. Can be null.
     302         * @since xxx
     303         */
     304        public Map<Integer, Entry<Layer, Element>> getSubLayers() {
     305            return subLayers;
     306        }
     307
     308        /**
    282309         * Return an InputStream for a URI from a .jos/.joz file.
    283310         *
    284311         * The following forms are supported:
     
    506533        List<Integer> sorted = Utils.topologicalSort(deps);
    507534        final Map<Integer, Layer> layersMap = new TreeMap<>(Collections.reverseOrder());
    508535        final Map<Integer, SessionLayerImporter> importers = new HashMap<>();
    509         final Map<Integer, String> names = new HashMap<>();
    510536
    511537        progressMonitor.setTicksCount(sorted.size());
    512538        LAYER: for (int idx: sorted) {
     
    519545                return;
    520546            }
    521547            String name = e.getAttribute("name");
    522             names.put(idx, name);
    523548            if (!e.hasAttribute("type")) {
    524549                error(tr("missing mandatory attribute ''type'' for element ''layer''"));
    525550                return;
     
    595620                }
    596621
    597622                layersMap.put(idx, layer);
     623                setLayerAttributes(layer, e);
     624
     625                if (support.getSubLayers() != null) {
     626                    support.getSubLayers().forEach((Integer markerIndex, Entry<Layer, Element> entry) -> {
     627                        Layer subLayer = entry.getKey();
     628                        Element subElement = entry.getValue();
     629
     630                        layersMap.put(markerIndex, subLayer);
     631                        setLayerAttributes(subLayer, subElement);
     632                    });
     633                }
     634
    598635            }
    599636            progressMonitor.worked(1);
    600637        }
    601638
     639
    602640        layers = new ArrayList<>();
    603641        for (Entry<Integer, Layer> entry : layersMap.entrySet()) {
    604642            Layer layer = entry.getValue();
    605             if (layer == null) {
    606                 continue;
     643            if (layer != null) {
     644                layers.add(layer);
    607645            }
    608             Element el = elems.get(entry.getKey());
    609             if (el.hasAttribute("visible")) {
    610                 layer.setVisible(Boolean.parseBoolean(el.getAttribute("visible")));
     646        }
     647    }
     648
     649    private static void setLayerAttributes(Layer layer, Element e) {
     650        if (layer == null)
     651            return;
     652
     653        if (e.hasAttribute("name")) {
     654            layer.setName(e.getAttribute("name"));
     655        }
     656        if (e.hasAttribute("visible")) {
     657            layer.setVisible(Boolean.parseBoolean(e.getAttribute("visible")));
     658        }
     659        if (e.hasAttribute("opacity")) {
     660            try {
     661                double opacity = Double.parseDouble(e.getAttribute("opacity"));
     662                layer.setOpacity(opacity);
     663            } catch (NumberFormatException ex) {
     664                Logging.warn(ex);
    611665            }
    612             if (el.hasAttribute("opacity")) {
    613                 try {
    614                     double opacity = Double.parseDouble(el.getAttribute("opacity"));
    615                     layer.setOpacity(opacity);
    616                 } catch (NumberFormatException ex) {
    617                     Logging.warn(ex);
    618                 }
    619             }
    620             layer.setName(names.get(entry.getKey()));
    621             layers.add(layer);
    622666        }
    623667    }
    624668
  • src/org/openstreetmap/josm/io/session/SessionWriter.java

     
    174174        }
    175175
    176176        /**
     177         * Get the index of the specified layer
     178         * @param layer the layer
     179         * @return the index of the specified layer
     180         * @since xxx
     181         */
     182        public int getLayerIndexOf(Layer layer) {
     183            return layers.indexOf(layer) + 1;
     184        }
     185
     186        /**
    177187         * Create a file inside the zip archive.
    178188         *
    179189         * @param zipPath the path inside the zip archive, e.g. "layers/03/data.xml"
     
    234244            SessionLayerExporter exporter = exporters.get(layer);
    235245            ExportSupport support = new ExportSupport(doc, index+1);
    236246            Element el = exporter.export(support);
     247            if (el == null) continue;
    237248            el.setAttribute("index", Integer.toString(index+1));
    238249            el.setAttribute("name", layer.getName());
    239250            el.setAttribute("visible", Boolean.toString(layer.isVisible()));
  • src/org/openstreetmap/josm/tools/ListenerList.java

     
    143143     * @return <code>true</code> if any are registered.
    144144     */
    145145    public boolean hasListeners() {
    146         return !listeners.isEmpty();
     146        return !listeners.isEmpty() || weakListeners.stream().map(l -> l.listener.get()).anyMatch(Objects::nonNull);
    147147    }
    148148
    149149    /**
  • test/data/sessions/gpx_markers_combined.jos

     
     1<?xml version="1.0" encoding="utf-8"?>
     2<josm-session version="0.1">
     3    <viewport>
     4        <center lat="0.0" lon="0.0"/>
     5        <scale meter-per-pixel="10.000000"/>
     6    </viewport>
     7    <projection>
     8        <projection-choice>
     9            <id>core:mercator</id>
     10            <parameters/>
     11        </projection-choice>
     12        <code>EPSG:3857</code>
     13    </projection>
     14    <layers>
     15        <layer index="1" name="GPX layer name" type="tracks" version="0.1" visible="true">
     16            <file>layers/01/data.gpx</file>
     17            <markerLayer index="2" name="Marker layer name" opacity="0.5" visible="true"/>
     18        </layer>
     19    </layers>
     20</josm-session>
  • test/unit/org/openstreetmap/josm/actions/SessionSaveActionTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.actions;
     3
     4import static org.junit.Assert.assertFalse;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertTrue;
     7
     8import java.io.File;
     9import java.io.IOException;
     10import java.util.Arrays;
     11import java.util.Collections;
     12
     13import org.junit.jupiter.api.Test;
     14import org.junit.jupiter.api.extension.RegisterExtension;
     15import org.openstreetmap.josm.TestUtils;
     16import org.openstreetmap.josm.gui.MainApplication;
     17import org.openstreetmap.josm.gui.layer.GpxLayer;
     18import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     19import org.openstreetmap.josm.io.session.SessionWriterTest;
     20import org.openstreetmap.josm.testutils.JOSMTestRules;
     21import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
     22
     23import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     24
     25/**
     26 * Unit tests for class {@link SessionSaveAsAction}.
     27 */
     28class SessionSaveActionTest {
     29    /**
     30     * Setup test.
     31     */
     32    @RegisterExtension
     33    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     34    public JOSMTestRules test = new JOSMTestRules().main().projection();
     35
     36    /**
     37     * Unit test of {@link SessionSaveAction}
     38     * @throws IOException Temp file could not be created
     39     */
     40    @Test
     41    void testSaveAction() throws IOException {
     42        TestUtils.assumeWorkingJMockit();
     43
     44        File jos = File.createTempFile("session", ".jos");
     45        File joz = new File(jos.getAbsolutePath().replaceFirst(".jos$", ".joz"));
     46        assertTrue(jos.exists());
     47        assertFalse(joz.exists());
     48
     49        String overrideStr = "javax.swing.JLabel[,0,0,0x0,invalid,alignmentX=0.0,alignmentY=0.0,border=,flags=8388608,maximumSize=,minimumSize=,"
     50                + "preferredSize=,defaultIcon=,disabledIcon=,horizontalAlignment=LEADING,horizontalTextPosition=TRAILING,iconTextGap=4,"
     51                + "labelFor=,text=<html>The following layer has been removed since the session was last saved:<ul><li>OSM layer name</ul>"
     52                + "<br>You are about to overwrite the session file \"" + joz.getName()
     53                + "\". Would you like to proceed?,verticalAlignment=CENTER,verticalTextPosition=CENTER]";
     54
     55        SessionSaveAction saveAction = SessionSaveAction.getInstance();
     56        saveAction.setEnabled(true);
     57
     58        OsmDataLayer osm = SessionWriterTest.createOsmLayer();
     59        GpxLayer gpx = SessionWriterTest.createGpxLayer();
     60
     61        JOptionPaneSimpleMocker mocker = new JOptionPaneSimpleMocker(Collections.singletonMap(overrideStr, 0));
     62        SessionSaveAction.setCurrentSession(jos, false, Arrays.asList(gpx, osm)); //gpx and OSM layer
     63        MainApplication.getLayerManager().addLayer(gpx); //only gpx layer
     64        saveAction.actionPerformed(null); //Complain that OSM layer was removed
     65        assertEquals(1, mocker.getInvocationLog().size());
     66        assertFalse(jos.exists());
     67        assertTrue(joz.exists()); //converted jos to joz since the session includes files
     68
     69        mocker = new JOptionPaneSimpleMocker(Collections.singletonMap(overrideStr, 0));
     70        joz.delete();
     71        saveAction.actionPerformed(null); //Do not complain about removed layers
     72        assertEquals(0, mocker.getInvocationLog().size());
     73        assertTrue(joz.exists());
     74
     75        joz.delete();
     76    }
     77}
  • test/unit/org/openstreetmap/josm/actions/SessionSaveAsActionTest.java

    Property changes on: test/unit/org/openstreetmap/josm/actions/SessionSaveActionTest.java
    ___________________________________________________________________
    Added: svn:mime-type
    ## -0,0 +1 ##
    +text/plain
     
    33
    44import static org.junit.jupiter.api.Assertions.assertFalse;
    55
     6import org.junit.jupiter.api.Test;
    67import org.junit.jupiter.api.extension.RegisterExtension;
    7 import org.junit.jupiter.api.Test;
    88import org.openstreetmap.josm.testutils.JOSMTestRules;
    99
    1010import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    1919     */
    2020    @RegisterExtension
    2121    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    22     public JOSMTestRules test = new JOSMTestRules();
     22    public JOSMTestRules test = new JOSMTestRules().main();
    2323
    2424    /**
    2525     * Unit test of {@link SessionSaveAsAction#actionPerformed}
  • test/unit/org/openstreetmap/josm/io/session/SessionWriterTest.java

     
    5050/**
    5151 * Unit tests for Session writing.
    5252 */
    53 class SessionWriterTest {
     53public class SessionWriterTest {
    5454
    5555    protected static final class OsmHeadlessJosExporter extends OsmDataSessionExporter {
    5656        public OsmHeadlessJosExporter(OsmDataLayer layer) {
     
    122122        }
    123123        for (final Layer l : layers) {
    124124            SessionLayerExporter s = SessionWriter.getSessionLayerExporter(l);
     125            s.getExportPanel();
    125126            exporters.put(l, s);
    126127            if (s instanceof GpxTracksSessionExporter) {
    127128                ((GpxTracksSessionExporter) s).setMetaTime(Instant.parse("2021-10-16T18:27:12.351Z"));
     
    153154        }
    154155    }
    155156
    156     private OsmDataLayer createOsmLayer() {
     157    /**
     158     * Creates an OSM layer
     159     * @return OSM layer
     160     * @since xxx
     161     */
     162    public static OsmDataLayer createOsmLayer() {
    157163        OsmDataLayer layer = new OsmDataLayer(new DataSet(), "OSM layer name", null);
    158164        layer.setAssociatedFile(new File("data.osm"));
    159165        return layer;
    160166    }
    161167
    162     private GpxLayer createGpxLayer() {
     168    /**
     169     * Creates a GPX layer
     170     * @return GPX layer
     171     * @since xxx
     172     */
     173    public static GpxLayer createGpxLayer() {
    163174        GpxData data = new GpxData();
    164175        WayPoint wp = new WayPoint(new LatLon(42.72665, -0.00747));
    165176        wp.setInstant(Instant.parse("2021-01-01T10:15:30.00Z"));
     
    170181        return layer;
    171182    }
    172183
    173     private MarkerLayer createMarkerLayer(GpxLayer gpx) {
     184    /**
     185     * Creates a MarkerLayer
     186     * @param gpx linked GPX layer
     187     * @return MarkerLayer
     188     * @since xxx
     189     */
     190    public static MarkerLayer createMarkerLayer(GpxLayer gpx) {
    174191        MarkerLayer layer = new MarkerLayer(gpx.data, "Marker layer name", gpx.getAssociatedFile(), gpx);
    175192        layer.setOpacity(0.5);
    176193        layer.setColor(new Color(0x12345678, true));
     194        gpx.setLinkedMarkerLayer(layer);
    177195        return layer;
    178196    }
    179197
    180     private ImageryLayer createImageryLayer() {
     198    /**
     199     * Creates an ImageryLayer
     200     * @return ImageryLayer
     201     * @since xxx
     202     */
     203    public static ImageryLayer createImageryLayer() {
    181204        TMSLayer layer = new TMSLayer(new ImageryInfo("the name", "http://www.url.com/"));
    182205        layer.getDisplaySettings().setOffsetBookmark(
    183206                new OffsetBookmark(ProjectionRegistry.getProjection().toCode(), layer.getInfo().getId(), layer.getInfo().getName(), "", 12, 34));
     
    184207        return layer;
    185208    }
    186209
    187     private NoteLayer createNoteLayer() {
     210    /**
     211     * Creates a NoteLayer
     212     * @return NoteLayer
     213     * @since xxx
     214     */
     215    public static NoteLayer createNoteLayer() {
    188216        return new NoteLayer(Arrays.asList(new Note(LatLon.ZERO)), "layer name");
    189217    }
    190218
     
    249277    @Test
    250278    void testWriteGpxAndMarkerJoz() throws IOException {
    251279        GpxLayer gpx = createGpxLayer();
    252         Map<String, byte[]> bytes = testWrite(Arrays.asList(gpx, createMarkerLayer(gpx)), true);
     280        MarkerLayer markers = createMarkerLayer(gpx);
     281        Map<String, byte[]> bytes = testWrite(Arrays.asList(gpx, markers), true);
    253282
    254         Path path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/gpx_markers.jos");
     283        Path path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/gpx_markers_combined.jos");
    255284        String expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
    256285        String actual = new String(bytes.get("session.jos"), StandardCharsets.UTF_8).replace("\r", "");
    257286        assertEquals(expected, actual);
     
    261290        actual = new String(bytes.get("layers/01/data.gpx"), StandardCharsets.UTF_8).replace("\r", "");
    262291        assertEquals(expected, actual);
    263292
     293        //Test writing when the marker layer has no corresponding GPX layer:
     294        gpx.setLinkedMarkerLayer(null);
     295        markers.fromLayer = null;
     296        markers.data.transferLayerPrefs(gpx.data.getLayerPrefs());
     297        bytes = testWrite(Arrays.asList(gpx, markers), true);
     298
     299        path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/gpx_markers.jos");
     300        expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
     301        actual = new String(bytes.get("session.jos"), StandardCharsets.UTF_8).replace("\r", "");
     302        assertEquals(expected, actual);
     303
     304        path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/data_export.gpx");
     305        expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
     306        actual = new String(bytes.get("layers/01/data.gpx"), StandardCharsets.UTF_8).replace("\r", "");
     307        assertEquals(expected, actual);
     308
    264309        path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/markers.gpx");
    265310        expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
    266311        actual = new String(bytes.get("layers/02/data.gpx"), StandardCharsets.UTF_8).replace("\r", "");
    267312        assertEquals(expected, actual);
     313
    268314    }
    269315
    270316    /**