001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.GridBagLayout;
010import java.awt.event.ActionEvent;
011import java.io.File;
012import java.io.IOException;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.List;
019import java.util.Map;
020import java.util.Set;
021import java.util.stream.Collectors;
022import java.util.stream.Stream;
023
024import javax.swing.BorderFactory;
025import javax.swing.JCheckBox;
026import javax.swing.JFileChooser;
027import javax.swing.JLabel;
028import javax.swing.JOptionPane;
029import javax.swing.JPanel;
030import javax.swing.JScrollPane;
031import javax.swing.JTabbedPane;
032import javax.swing.SwingConstants;
033import javax.swing.border.EtchedBorder;
034import javax.swing.filechooser.FileFilter;
035
036import org.openstreetmap.josm.data.preferences.BooleanProperty;
037import org.openstreetmap.josm.gui.ExtendedDialog;
038import org.openstreetmap.josm.gui.HelpAwareOptionPane;
039import org.openstreetmap.josm.gui.MainApplication;
040import org.openstreetmap.josm.gui.MapFrame;
041import org.openstreetmap.josm.gui.MapFrameListener;
042import org.openstreetmap.josm.gui.Notification;
043import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
044import org.openstreetmap.josm.gui.layer.Layer;
045import org.openstreetmap.josm.gui.util.WindowGeometry;
046import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
047import org.openstreetmap.josm.io.session.SessionLayerExporter;
048import org.openstreetmap.josm.io.session.SessionWriter;
049import org.openstreetmap.josm.tools.GBC;
050import org.openstreetmap.josm.tools.JosmRuntimeException;
051import org.openstreetmap.josm.tools.Logging;
052import org.openstreetmap.josm.tools.MultiMap;
053import org.openstreetmap.josm.tools.UserCancelException;
054import org.openstreetmap.josm.tools.Utils;
055
056/**
057 * Saves a JOSM session
058 * @since 4685
059 */
060public class SessionSaveAsAction extends DiskAccessAction implements MapFrameListener {
061
062    private transient List<Layer> layers;
063    private transient Map<Layer, SessionLayerExporter> exporters;
064    private transient MultiMap<Layer, Layer> dependencies;
065
066    private static final BooleanProperty SAVE_LOCAL_FILES_PROPERTY = new BooleanProperty("session.savelocal", true);
067
068    /**
069     * Constructs a new {@code SessionSaveAsAction}.
070     */
071    public SessionSaveAsAction() {
072        this(true, false);
073        updateEnabledState();
074    }
075
076    /**
077     * Constructs a new {@code SessionSaveAsAction}.
078     * @param toolbar Register this action for the toolbar preferences?
079     * @param installAdapters False, if you don't want to install layer changed and selection changed adapters
080     */
081    protected SessionSaveAsAction(boolean toolbar, boolean installAdapters) {
082        super(tr("Save Session As..."), "session", tr("Save the current session to a new file."),
083                null, toolbar, "save_as-session", installAdapters);
084        setHelpId(ht("/Action/SessionSaveAs"));
085        MainApplication.addMapFrameListener(this);
086    }
087
088    @Override
089    public void actionPerformed(ActionEvent e) {
090        try {
091            saveSession();
092        } catch (UserCancelException ignore) {
093            Logging.trace(ignore);
094        }
095    }
096
097    @Override
098    public void destroy() {
099        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    }
342}