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;
021
022import javax.swing.BorderFactory;
023import javax.swing.JCheckBox;
024import javax.swing.JFileChooser;
025import javax.swing.JLabel;
026import javax.swing.JOptionPane;
027import javax.swing.JPanel;
028import javax.swing.JScrollPane;
029import javax.swing.JTabbedPane;
030import javax.swing.SwingConstants;
031import javax.swing.border.EtchedBorder;
032import javax.swing.filechooser.FileFilter;
033
034import org.openstreetmap.josm.gui.ExtendedDialog;
035import org.openstreetmap.josm.gui.HelpAwareOptionPane;
036import org.openstreetmap.josm.gui.MainApplication;
037import org.openstreetmap.josm.gui.MapFrame;
038import org.openstreetmap.josm.gui.MapFrameListener;
039import org.openstreetmap.josm.gui.layer.Layer;
040import org.openstreetmap.josm.gui.util.WindowGeometry;
041import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
042import org.openstreetmap.josm.io.session.SessionLayerExporter;
043import org.openstreetmap.josm.io.session.SessionWriter;
044import org.openstreetmap.josm.tools.GBC;
045import org.openstreetmap.josm.tools.Logging;
046import org.openstreetmap.josm.tools.MultiMap;
047import org.openstreetmap.josm.tools.UserCancelException;
048import org.openstreetmap.josm.tools.Utils;
049
050/**
051 * Saves a JOSM session
052 * @since 4685
053 */
054public class SessionSaveAsAction extends DiskAccessAction implements MapFrameListener {
055
056    private transient List<Layer> layers;
057    private transient Map<Layer, SessionLayerExporter> exporters;
058    private transient MultiMap<Layer, Layer> dependencies;
059
060    /**
061     * Constructs a new {@code SessionSaveAsAction}.
062     */
063    public SessionSaveAsAction() {
064        this(true, true);
065    }
066
067    /**
068     * Constructs a new {@code SessionSaveAsAction}.
069     * @param toolbar Register this action for the toolbar preferences?
070     * @param installAdapters False, if you don't want to install layer changed and selection changed adapters
071     */
072    protected SessionSaveAsAction(boolean toolbar, boolean installAdapters) {
073        super(tr("Save Session As..."), "session", tr("Save the current session to a new file."),
074                null, toolbar, "save_as-session", installAdapters);
075        setHelpId(ht("/Action/SessionSaveAs"));
076        MainApplication.addMapFrameListener(this);
077    }
078
079    @Override
080    public void actionPerformed(ActionEvent e) {
081        try {
082            saveSession();
083        } catch (UserCancelException ignore) {
084            Logging.trace(ignore);
085        }
086    }
087
088    @Override
089    public void destroy() {
090        MainApplication.removeMapFrameListener(this);
091        super.destroy();
092    }
093
094    /**
095     * Attempts to save the session.
096     * @throws UserCancelException when the user has cancelled the save process.
097     * @since 8913
098     */
099    public void saveSession() throws UserCancelException {
100        if (!isEnabled()) {
101            return;
102        }
103
104        SessionSaveAsDialog dlg = new SessionSaveAsDialog();
105        dlg.showDialog();
106        if (dlg.getValue() != 1) {
107            throw new UserCancelException();
108        }
109
110        boolean zipRequired = false;
111        for (Layer l : layers) {
112            SessionLayerExporter ex = exporters.get(l);
113            if (ex != null && ex.requiresZip()) {
114                zipRequired = true;
115                break;
116            }
117        }
118
119        FileFilter joz = new ExtensionFileFilter("joz", "joz", tr("Session file (archive) (*.joz)"));
120        FileFilter jos = new ExtensionFileFilter("jos", "jos", tr("Session file (*.jos)"));
121
122        AbstractFileChooser fc;
123
124        if (zipRequired) {
125            fc = createAndOpenFileChooser(false, false, tr("Save Session"), joz, JFileChooser.FILES_ONLY, "lastDirectory");
126        } else {
127            fc = createAndOpenFileChooser(false, false, tr("Save Session"), Arrays.asList(jos, joz), jos,
128                    JFileChooser.FILES_ONLY, "lastDirectory");
129        }
130
131        if (fc == null) {
132            throw new UserCancelException();
133        }
134
135        File file = fc.getSelectedFile();
136        String fn = file.getName();
137
138        boolean zip;
139        FileFilter ff = fc.getFileFilter();
140        if (zipRequired || joz.equals(ff)) {
141            zip = true;
142        } else if (jos.equals(ff)) {
143            zip = false;
144        } else {
145            if (Utils.hasExtension(fn, "joz")) {
146                zip = true;
147            } else {
148                zip = false;
149            }
150        }
151        if (fn.indexOf('.') == -1) {
152            file = new File(file.getPath() + (zip ? ".joz" : ".jos"));
153            if (!SaveActionBase.confirmOverwrite(file)) {
154                throw new UserCancelException();
155            }
156        }
157
158        List<Layer> layersOut = new ArrayList<>();
159        for (Layer layer : layers) {
160            if (exporters.get(layer) == null || !exporters.get(layer).shallExport()) continue;
161            // TODO: resolve dependencies for layers excluded by the user
162            layersOut.add(layer);
163        }
164
165        int active = -1;
166        Layer activeLayer = getLayerManager().getActiveLayer();
167        if (activeLayer != null) {
168            active = layersOut.indexOf(activeLayer);
169        }
170
171        SessionWriter sw = new SessionWriter(layersOut, active, exporters, dependencies, zip);
172        try {
173            sw.write(file);
174            SaveActionBase.addToFileOpenHistory(file);
175        } catch (IOException ex) {
176            Logging.error(ex);
177            HelpAwareOptionPane.showMessageDialogInEDT(
178                    MainApplication.getMainFrame(),
179                    tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>",
180                            file.getName(), Utils.escapeReservedCharactersHTML(ex.getMessage())),
181                    tr("IO Error"),
182                    JOptionPane.ERROR_MESSAGE,
183                    null
184            );
185        }
186    }
187
188    /**
189     * The "Save Session" dialog
190     */
191    public class SessionSaveAsDialog extends ExtendedDialog {
192
193        /**
194         * Constructs a new {@code SessionSaveAsDialog}.
195         */
196        public SessionSaveAsDialog() {
197            super(MainApplication.getMainFrame(), tr("Save Session"), tr("Save As"), tr("Cancel"));
198            configureContextsensitiveHelp("Action/SessionSaveAs", true /* show help button */);
199            initialize();
200            setButtonIcons("save_as", "cancel");
201            setDefaultButton(1);
202            setRememberWindowGeometry(getClass().getName() + ".geometry",
203                    WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(450, 450)));
204            setContent(build(), false);
205        }
206
207        /**
208         * Initializes action.
209         */
210        public final void initialize() {
211            layers = new ArrayList<>(getLayerManager().getLayers());
212            exporters = new HashMap<>();
213            dependencies = new MultiMap<>();
214
215            Set<Layer> noExporter = new HashSet<>();
216
217            for (Layer layer : layers) {
218                SessionLayerExporter exporter = SessionWriter.getSessionLayerExporter(layer);
219                if (exporter != null) {
220                    exporters.put(layer, exporter);
221                    Collection<Layer> deps = exporter.getDependencies();
222                    if (deps != null) {
223                        dependencies.putAll(layer, deps);
224                    } else {
225                        dependencies.putVoid(layer);
226                    }
227                } else {
228                    noExporter.add(layer);
229                    exporters.put(layer, null);
230                }
231            }
232
233            int numNoExporter = 0;
234            WHILE: while (numNoExporter != noExporter.size()) {
235                numNoExporter = noExporter.size();
236                for (Layer layer : layers) {
237                    if (noExporter.contains(layer)) continue;
238                    for (Layer depLayer : dependencies.get(layer)) {
239                        if (noExporter.contains(depLayer)) {
240                            noExporter.add(layer);
241                            exporters.put(layer, null);
242                            break WHILE;
243                        }
244                    }
245                }
246            }
247        }
248
249        protected final Component build() {
250            JPanel ip = new JPanel(new GridBagLayout());
251            for (Layer layer : layers) {
252                JPanel wrapper = new JPanel(new GridBagLayout());
253                wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
254                Component exportPanel;
255                SessionLayerExporter exporter = exporters.get(layer);
256                if (exporter == null) {
257                    if (!exporters.containsKey(layer)) throw new AssertionError();
258                    exportPanel = getDisabledExportPanel(layer);
259                } else {
260                    exportPanel = exporter.getExportPanel();
261                }
262                wrapper.add(exportPanel, GBC.std().fill(GBC.HORIZONTAL));
263                ip.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 2, 4, 2));
264            }
265            ip.add(GBC.glue(0, 1), GBC.eol().fill(GBC.VERTICAL));
266            JScrollPane sp = new JScrollPane(ip);
267            sp.setBorder(BorderFactory.createEmptyBorder());
268            JPanel p = new JPanel(new GridBagLayout());
269            p.add(sp, GBC.eol().fill());
270            final JTabbedPane tabs = new JTabbedPane();
271            tabs.addTab(tr("Layers"), p);
272            return tabs;
273        }
274
275        protected final Component getDisabledExportPanel(Layer layer) {
276            JPanel p = new JPanel(new GridBagLayout());
277            JCheckBox include = new JCheckBox();
278            include.setEnabled(false);
279            JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEFT);
280            lbl.setToolTipText(tr("No exporter for this layer"));
281            lbl.setLabelFor(include);
282            lbl.setEnabled(false);
283            p.add(include, GBC.std());
284            p.add(lbl, GBC.std());
285            p.add(GBC.glue(1, 0), GBC.std().fill(GBC.HORIZONTAL));
286            return p;
287        }
288    }
289
290    @Override
291    protected void updateEnabledState() {
292        setEnabled(MainApplication.isDisplayingMapView());
293    }
294
295    @Override
296    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
297        updateEnabledState();
298    }
299}