Ticket #21923: all.patch

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

     
    10941094
    10951095    @Override
    10961096    public void put(String key, Object value) {
     1097        put(key, value, true);
     1098    }
     1099
     1100    /**
     1101     * Put a key / value pair as a new attribute. Overrides key / value pair with the same key (if present).
     1102     * Only sets the modified state when setModified is true.
     1103     *
     1104     * @param key the key
     1105     * @param value the value
     1106     * @param setModified whether to change the modified state
     1107     */
     1108    public void put(String key, Object value, boolean setModified) {
    10971109        super.put(key, value);
    1098         invalidate();
     1110        fireInvalidate(setModified);
    10991111    }
    11001112
    11011113    /**
     
    11321144    }
    11331145
    11341146    private void fireInvalidate(boolean setModified) {
     1147        if (setModified) {
     1148            setModified(true);
     1149        }
    11351150        if (updating || initializing) {
    11361151            suppressedInvalidate = true;
    11371152        } else {
    1138             if (setModified) {
    1139                 setModified(true);
    1140             }
    11411153            if (listeners.hasListeners()) {
    11421154                GpxDataChangeEvent e = new GpxDataChangeEvent(this);
    11431155                listeners.fireEvent(l -> l.gpxDataChanged(e));
     
    11581170     * @since 15496
    11591171     */
    11601172    public void endUpdate() {
    1161         boolean setModified = updating;
    11621173        updating = initializing = false;
    11631174        if (suppressedInvalidate) {
    1164             fireInvalidate(setModified);
     1175            fireInvalidate(false);
    11651176            suppressedInvalidate = false;
    11661177        }
    11671178    }
  • src/org/openstreetmap/josm/data/gpx/GpxTrack.java

     
    8282
    8383    @Override
    8484    public void setColor(Color color) {
    85         setColorExtension(color);
     85        setColorExtensionGPXD(color, true);
    8686        colorCache = color;
    8787    }
    8888
    89     private void setColorExtension(Color color) {
     89    private void setColorExtensionGPXD(Color color, boolean invalidate) {
    9090        getExtensions().findAndRemove("gpxx", "DisplayColor");
    9191        if (color == null) {
    9292            getExtensions().findAndRemove("gpxd", "color");
    9393        } else {
    9494            getExtensions().addOrUpdate("gpxd", "color", String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue()));
    9595        }
    96         fireInvalidate();
     96        colorFormat = ColorFormat.GPXD;
     97        if (invalidate) {
     98            fireInvalidate();
     99        }
    97100    }
    98101
    99102    @Override
     
    167170                closestGarminColorCache.put(c, colorString);
    168171                getExtensions().addIfNotPresent("gpxx", "TrackExtension").getExtensions().addOrUpdate("gpxx", "DisplayColor", colorString);
    169172            } else if (cFormat == ColorFormat.GPXD) {
    170                 setColor(c);
     173                setColorExtensionGPXD(c, false);
    171174            }
    172175            colorFormat = cFormat;
    173176        }
  • 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/GpxWriter.java

     
    123123                e.put("value", entry.getValue());
    124124            });
    125125        }
    126         data.put(META_TIME, (metaTime != null ? metaTime : Instant.now()).toString());
     126        data.put(META_TIME, (metaTime != null ? metaTime : Instant.now()).toString(), false);
    127127        data.endUpdate();
    128128
    129129        Collection<IWithAttributes> all = new ArrayList<>();
  • 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();
     50    }
     51
     52    @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;
    3578    }
    3679
    3780    @Override
  • 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;
    607             }
    608             Element el = elems.get(entry.getKey());
    609             if (el.hasAttribute("visible")) {
    610                 layer.setVisible(Boolean.parseBoolean(el.getAttribute("visible")));
     643            if (layer != null) {
     644                layers.add(layer);
    611645            }
    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                 }
     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);
    619665            }
    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
    6 import org.junit.jupiter.api.extension.RegisterExtension;
    76import org.junit.jupiter.api.Test;
     7import org.junit.jupiter.api.extension.RegisterExtension;
    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     * Returns OSM layer
     159     * @return OSM layer
     160     */
     161    public static OsmDataLayer createOsmLayer() {
    157162        OsmDataLayer layer = new OsmDataLayer(new DataSet(), "OSM layer name", null);
    158163        layer.setAssociatedFile(new File("data.osm"));
    159164        return layer;
    160165    }
    161166
    162     private GpxLayer createGpxLayer() {
     167    /**Returns GPX layer
     168     * @return GPX layer
     169     */
     170    public static GpxLayer createGpxLayer() {
    163171        GpxData data = new GpxData();
    164172        WayPoint wp = new WayPoint(new LatLon(42.72665, -0.00747));
    165173        wp.setInstant(Instant.parse("2021-01-01T10:15:30.00Z"));
     
    170178        return layer;
    171179    }
    172180
    173     private MarkerLayer createMarkerLayer(GpxLayer gpx) {
     181    /**
     182     * Returns MarkerLayer
     183     * @param gpx linked GPX layer
     184     * @return MarkerLayer
     185     */
     186    public static MarkerLayer createMarkerLayer(GpxLayer gpx) {
    174187        MarkerLayer layer = new MarkerLayer(gpx.data, "Marker layer name", gpx.getAssociatedFile(), gpx);
    175188        layer.setOpacity(0.5);
    176189        layer.setColor(new Color(0x12345678, true));
     190        gpx.setLinkedMarkerLayer(layer);
    177191        return layer;
    178192    }
    179193
    180     private ImageryLayer createImageryLayer() {
     194    /**
     195     * Returns ImageryLayer
     196     * @return ImageryLayer
     197     */
     198    public static ImageryLayer createImageryLayer() {
    181199        TMSLayer layer = new TMSLayer(new ImageryInfo("the name", "http://www.url.com/"));
    182200        layer.getDisplaySettings().setOffsetBookmark(
    183201                new OffsetBookmark(ProjectionRegistry.getProjection().toCode(), layer.getInfo().getId(), layer.getInfo().getName(), "", 12, 34));
    184202        return layer;
    185203    }
    186204
    187     private NoteLayer createNoteLayer() {
     205    /**
     206     * Returns NoteLayer
     207     * @return NoteLayer
     208     */
     209    public static NoteLayer createNoteLayer() {
    188210        return new NoteLayer(Arrays.asList(new Note(LatLon.ZERO)), "layer name");
    189211    }
    190212
     
    249271    @Test
    250272    void testWriteGpxAndMarkerJoz() throws IOException {
    251273        GpxLayer gpx = createGpxLayer();
    252         Map<String, byte[]> bytes = testWrite(Arrays.asList(gpx, createMarkerLayer(gpx)), true);
     274        MarkerLayer markers = createMarkerLayer(gpx);
     275        Map<String, byte[]> bytes = testWrite(Arrays.asList(gpx, markers), true);
    253276
    254         Path path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/gpx_markers.jos");
     277        Path path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/gpx_markers_combined.jos");
    255278        String expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
    256279        String actual = new String(bytes.get("session.jos"), StandardCharsets.UTF_8).replace("\r", "");
    257280        assertEquals(expected, actual);
     
    261284        actual = new String(bytes.get("layers/01/data.gpx"), StandardCharsets.UTF_8).replace("\r", "");
    262285        assertEquals(expected, actual);
    263286
     287        //Test writing when the marker layer has no corresponding GPX layer:
     288        gpx.setLinkedMarkerLayer(null);
     289        markers.fromLayer = null;
     290        markers.data.transferLayerPrefs(gpx.data.getLayerPrefs());
     291        bytes = testWrite(Arrays.asList(gpx, markers), true);
     292
     293        path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/gpx_markers.jos");
     294        expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
     295        actual = new String(bytes.get("session.jos"), StandardCharsets.UTF_8).replace("\r", "");
     296        assertEquals(expected, actual);
     297
     298        path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/data_export.gpx");
     299        expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
     300        actual = new String(bytes.get("layers/01/data.gpx"), StandardCharsets.UTF_8).replace("\r", "");
     301        assertEquals(expected, actual);
     302
    264303        path = Paths.get(TestUtils.getTestDataRoot() + "/sessions/markers.gpx");
    265304        expected = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).replace("\r", "");
    266305        actual = new String(bytes.get("layers/02/data.gpx"), StandardCharsets.UTF_8).replace("\r", "");
    267306        assertEquals(expected, actual);
     307
    268308    }
    269309
    270310    /**