// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.io;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.AbstractAction;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.UploadAction;
import org.openstreetmap.josm.data.APIDataSet;
import org.openstreetmap.josm.gui.ExceptionDialogUtil;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.WindowGeometry;
public class SaveLayersDialog extends JDialog {
static public enum UserAction {
/**
* save/upload layers was successful, proceed with operation
*/
PROCEED,
/**
* save/upload of layers was not successful or user cancelled
* operation
*/
CANCEL
}
private SaveLayersModel model;
private UserAction action = UserAction.CANCEL;
private UploadAndSaveProgressRenderer pnlUploadLayers;
private SaveAndProceedAction saveAndProceedAction;
private DiscardAndProceedAction discardAndProceedAction;
private CancelAction cancelAction;
private SaveAndUploadTask saveAndUploadTask;
/**
* builds the GUI
*/
protected void build() {
WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(600,300));
geometry.apply(this);
getContentPane().setLayout(new BorderLayout());
model = new SaveLayersModel();
SaveLayersTable table;
JScrollPane pane = new JScrollPane(table = new SaveLayersTable(model));
model.addPropertyChangeListener(table);
getContentPane().add(pane, BorderLayout.CENTER);
getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
table.getTableHeader().setPreferredSize(new Dimension(table.getTableHeader().getWidth(), 40));
addWindowListener(new WindowClosingAdapter());
}
/**
* builds the button row
*
* @return the panel with the button row
*/
protected JPanel buildButtonRow() {
JPanel pnl = new JPanel();
pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
saveAndProceedAction = new SaveAndProceedAction();
model.addPropertyChangeListener(saveAndProceedAction);
pnl.add(new SideButton(saveAndProceedAction));
discardAndProceedAction = new DiscardAndProceedAction();
model.addPropertyChangeListener(discardAndProceedAction);
pnl.add(new SideButton(discardAndProceedAction));
cancelAction = new CancelAction();
pnl.add(new SideButton(cancelAction));
JPanel pnl2 = new JPanel();
pnl2.setLayout(new BorderLayout());
pnl2.add(pnlUploadLayers = new UploadAndSaveProgressRenderer(), BorderLayout.CENTER);
model.addPropertyChangeListener(pnlUploadLayers);
pnl2.add(pnl, BorderLayout.SOUTH);
return pnl2;
}
public void prepareForSavingAndUpdatingLayersBeforeExit() {
setTitle(tr("Unsaved changes - Save/Upload before exiting?"));
this.saveAndProceedAction.initForSaveAndExit();
this.discardAndProceedAction.initForDiscardAndExit();
}
public void prepareForSavingAndUpdatingLayersBeforeDelete() {
setTitle(tr("Unsaved changes - Save/Upload before deleting?"));
this.saveAndProceedAction.initForSaveAndDelete();
this.discardAndProceedAction.initForDiscardAndDelete();
}
public SaveLayersDialog(Component parent) {
super(JOptionPane.getFrameForComponent(parent), true /* modal */);
build();
}
public UserAction getUserAction() {
return this.action;
}
public SaveLayersModel getModel() {
return model;
}
protected void launchSafeAndUploadTask() {
ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers);
monitor.beginTask(tr("Uploading and saving modified layers ..."));
this.saveAndUploadTask = new SaveAndUploadTask(model, monitor);
new Thread(saveAndUploadTask).start();
}
protected void cancelSafeAndUploadTask() {
if (this.saveAndUploadTask != null) {
this.saveAndUploadTask.cancel();
}
model.setMode(Mode.EDITING_DATA);
}
private static class LayerListWarningMessagePanel extends JPanel {
private JLabel lblMessage;
private JList lstLayers;
protected void build() {
setLayout(new GridBagLayout());
GridBagConstraints gc = new GridBagConstraints();
gc.gridx = 0;
gc.gridy = 0;
gc.fill = GridBagConstraints.HORIZONTAL;
gc.weightx = 1.0;
gc.weighty = 0.0;
add(lblMessage = new JLabel(), gc);
lblMessage.setHorizontalAlignment(JLabel.LEFT);
lstLayers = new JList();
lstLayers.setCellRenderer(
new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
SaveLayerInfo info = (SaveLayerInfo)value;
setIcon(info.getLayer().getIcon());
setText(info.getName());
return this;
}
}
);
gc.gridx = 0;
gc.gridy = 1;
gc.fill = GridBagConstraints.HORIZONTAL;
gc.weightx = 1.0;
gc.weighty = 1.0;
add(lstLayers,gc);
}
public LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) {
build();
lblMessage.setText(msg);
lstLayers.setListData(infos.toArray());
}
}
protected void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) {
String msg = trn("<html>{0} layer has unresolved conflicts.<br>"
+ "Either resolve them first or discard the modifications.<br>"
+ "Layer with conflicts:</html>",
"<html>{0} layers have unresolved conflicts.<br>"
+ "Either resolve them first or discard the modifications.<br>"
+ "Layers with conflicts:</html>",
infos.size(),
infos.size());
JOptionPane.showConfirmDialog(
Main.parent,
new LayerListWarningMessagePanel(msg, infos),
tr("Unsaved data and conflicts"),
JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE
);
}
protected void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) {
String msg = trn("<html>{0} layer needs saving but has no associated file.<br>"
+ "Either select a file for this layer or discard the changes.<br>"
+ "Layer without a file:</html>",
"<html>{0} layers need saving but have no associated file.<br>"
+ "Either select a file for each of them or discard the changes.<br>"
+ "Layers without a file:</html>",
infos.size(),
infos.size());
JOptionPane.showConfirmDialog(
Main.parent,
new LayerListWarningMessagePanel(msg, infos),
tr("Unsaved data and missing associated file"),
JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE
);
}
protected void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) {
String msg = trn("<html>{0} layer needs saving but has an associated file<br>"
+ "which cannot be written.<br>"
+ "Either select another file for this layer or discard the changes.<br>"
+ "Layer with a non-writable file:</html>",
"<html>{0} layers need saving but have associated files<br>"
+ "which cannot be written.<br>"
+ "Either select another file for each of them or discard the changes.<br>"
+ "Layers with non-writable files:</html>",
infos.size(),
infos.size());
JOptionPane.showConfirmDialog(
Main.parent,
new LayerListWarningMessagePanel(msg, infos),
tr("Unsaved data non-writable files"),
JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE
);
}
protected boolean confirmSaveLayerInfosOK() {
List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest();
if (!layerInfos.isEmpty()) {
warnLayersWithConflictsAndUploadRequest(layerInfos);
return false;
}
layerInfos = model.getLayersWithoutFilesAndSaveRequest();
if (!layerInfos.isEmpty()) {
warnLayersWithoutFilesAndSaveRequest(layerInfos);
return false;
}
layerInfos = model.getLayersWithIllegalFilesAndSaveRequest();
if (!layerInfos.isEmpty()) {
warnLayersWithIllegalFilesAndSaveRequest(layerInfos);
return false;
}
return true;
}
protected void setUserAction(UserAction action) {
this.action = action;
}
class WindowClosingAdapter extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
cancelAction.cancel();
}
}
class CancelAction extends AbstractAction {
public CancelAction() {
putValue(NAME, tr("Cancel"));
putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM"));
putValue(SMALL_ICON, ImageProvider.get("cancel"));
}
protected void cancelWhenInEditingModel() {
setUserAction(UserAction.CANCEL);
setVisible(false);
}
protected void cancelWhenInSaveAndUploadingMode() {
cancelSafeAndUploadTask();
}
public void cancel() {
switch(model.getMode()) {
case EDITING_DATA: cancelWhenInEditingModel(); break;
case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); break;
}
}
public void actionPerformed(ActionEvent e) {
cancel();
}
}
class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener {
public DiscardAndProceedAction() {
initForDiscardAndExit();
}
public void initForDiscardAndExit() {
putValue(NAME, tr("Discard and Exit"));
putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
putValue(SMALL_ICON, ImageProvider.get("exit"));
}
public void initForDiscardAndDelete() {
putValue(NAME, tr("Discard and Delete"));
putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
}
public void actionPerformed(ActionEvent e) {
setUserAction(UserAction.PROCEED);
setVisible(false);
}
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
Mode mode = (Mode)evt.getNewValue();
switch(mode) {
case EDITING_DATA: setEnabled(true); break;
case UPLOADING_AND_SAVING: setEnabled(false); break;
}
}
}
}
class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
public SaveAndProceedAction() {
initForSaveAndExit();
}
public void initForSaveAndExit() {
putValue(NAME, tr("Save/Upload and Exit"));
putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
putValue(SMALL_ICON, ImageProvider.get("exit"));
}
public void initForSaveAndDelete() {
putValue(NAME, tr("Save/Upload and Delete"));
putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
}
public void actionPerformed(ActionEvent e) {
if (! confirmSaveLayerInfosOK())
return;
launchSafeAndUploadTask();
}
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
SaveLayersModel.Mode mode = (SaveLayersModel.Mode)evt.getNewValue();
switch(mode) {
case EDITING_DATA: setEnabled(true); break;
case UPLOADING_AND_SAVING: setEnabled(false); break;
}
}
}
}
/**
* This is the asynchronous task which uploads modified layers to the server and
* saves them to files, if requested by the user.
*
*/
protected class SaveAndUploadTask implements Runnable {
private SaveLayersModel model;
private ProgressMonitor monitor;
private ExecutorService worker;
private boolean cancelled;
private Future<?> currentFuture;
private AbstractIOTask currentTask;
public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
this.model = model;
this.monitor = monitor;
this.worker = Executors.newSingleThreadExecutor();
}
protected void uploadLayers(List<SaveLayerInfo> toUpload) {
for (final SaveLayerInfo layerInfo: toUpload) {
if (cancelled) {
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
continue;
}
monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
if (!new UploadAction().checkPreUploadConditions(layerInfo.getLayer())) {
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
continue;
}
final UploadDialog dialog = UploadDialog.getUploadDialog();
dialog.setUploadedPrimitives(new APIDataSet(layerInfo.getLayer().data));
dialog.setVisible(true);
if (dialog.isCanceled()) {
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
continue;
}
dialog.rememberUserInput();
currentTask = new UploadLayerTask(
UploadDialog.getUploadDialog().getUploadStrategySpecification(),
layerInfo.getLayer(),
monitor,
UploadDialog.getUploadDialog().getChangeset()
);
currentFuture = worker.submit(currentTask);
try {
// wait for the asynchronous task to complete
//
currentFuture.get();
} catch(CancellationException e) {
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
} catch(Exception e) {
e.printStackTrace();
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
ExceptionDialogUtil.explainException(e);
}
if (currentTask.isCancelled()) {
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
} else if (currentTask.isFailed()) {
currentTask.getLastException().printStackTrace();
ExceptionDialogUtil.explainException(currentTask.getLastException());
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
} else {
model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.OK);
}
currentTask = null;
currentFuture = null;
}
}
protected void saveLayers(List<SaveLayerInfo> toSave) {
for (final SaveLayerInfo layerInfo: toSave) {
if (cancelled) {
model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
continue;
}
currentTask= new SaveLayerTask(layerInfo, monitor);
currentFuture = worker.submit(currentTask);
try {
// wait for the asynchronous task to complete
//
currentFuture.get();
} catch(CancellationException e) {
model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
} catch(Exception e) {
e.printStackTrace();
model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
ExceptionDialogUtil.explainException(e);
}
if (currentTask.isCancelled()) {
model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
} else if (currentTask.isFailed()) {
if (currentTask.getLastException() != null) {
currentTask.getLastException().printStackTrace();
ExceptionDialogUtil.explainException(currentTask.getLastException());
}
model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
} else {
model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
}
this.currentTask = null;
this.currentFuture = null;
}
}
protected void warnBecauseOfUnsavedData() {
int numProblems = model.getNumCancel() + model.getNumFailed();
if (numProblems == 0) return;
String msg = trn(
"<html>An upload and/or save operation of one layer with modifications<br>"
+ "was cancelled or has failed.</html>",
"<html>Upload and/or save operations of {0} layers with modifications<br>"
+ "were cancelled or have failed.</html>",
numProblems,
numProblems
);
JOptionPane.showMessageDialog(
Main.parent,
msg,
tr("Incomplete upload and/or save"),
JOptionPane.WARNING_MESSAGE
);
}
public void run() {
model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
List<SaveLayerInfo> toUpload = model.getLayersToUpload();
if (!toUpload.isEmpty()) {
uploadLayers(toUpload);
}
List<SaveLayerInfo> toSave = model.getLayersToSave();
if (!toSave.isEmpty()) {
saveLayers(toSave);
}
model.setMode(SaveLayersModel.Mode.EDITING_DATA);
if (model.hasUnsavedData()) {
warnBecauseOfUnsavedData();
model.setMode(Mode.EDITING_DATA);
if (cancelled) {
setUserAction(UserAction.CANCEL);
setVisible(false);
}
} else {
setUserAction(UserAction.PROCEED);
setVisible(false);
}
}
public void cancel() {
if (currentTask != null) {
currentTask.cancel();
}
cancelled = true;
}
}
}