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}