001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.BorderLayout;
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.Graphics2D;
012import java.awt.GridBagConstraints;
013import java.awt.GridBagLayout;
014import java.awt.Image;
015import java.awt.event.ActionEvent;
016import java.awt.event.WindowAdapter;
017import java.awt.event.WindowEvent;
018import java.awt.image.BufferedImage;
019import java.beans.PropertyChangeEvent;
020import java.beans.PropertyChangeListener;
021import java.util.List;
022import java.util.concurrent.CancellationException;
023import java.util.concurrent.ExecutorService;
024import java.util.concurrent.Executors;
025import java.util.concurrent.Future;
026
027import javax.swing.AbstractAction;
028import javax.swing.DefaultListCellRenderer;
029import javax.swing.ImageIcon;
030import javax.swing.JComponent;
031import javax.swing.JButton;
032import javax.swing.JDialog;
033import javax.swing.JLabel;
034import javax.swing.JList;
035import javax.swing.JOptionPane;
036import javax.swing.JPanel;
037import javax.swing.JScrollPane;
038import javax.swing.KeyStroke;
039import javax.swing.WindowConstants;
040import javax.swing.event.TableModelEvent;
041import javax.swing.event.TableModelListener;
042
043import org.openstreetmap.josm.Main;
044import org.openstreetmap.josm.actions.UploadAction;
045import org.openstreetmap.josm.data.APIDataSet;
046import org.openstreetmap.josm.gui.ExceptionDialogUtil;
047import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
048import org.openstreetmap.josm.gui.progress.ProgressMonitor;
049import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
050import org.openstreetmap.josm.tools.ImageProvider;
051import org.openstreetmap.josm.tools.WindowGeometry;
052
053public class SaveLayersDialog extends JDialog implements TableModelListener {
054    static public enum UserAction {
055        /**
056         * save/upload layers was successful, proceed with operation
057         */
058        PROCEED,
059        /**
060         * save/upload of layers was not successful or user canceled
061         * operation
062         */
063        CANCEL
064    }
065
066    private SaveLayersModel model;
067    private UserAction action = UserAction.CANCEL;
068    private UploadAndSaveProgressRenderer pnlUploadLayers;
069
070    private SaveAndProceedAction saveAndProceedAction;
071    private DiscardAndProceedAction discardAndProceedAction;
072    private CancelAction cancelAction;
073    private SaveAndUploadTask saveAndUploadTask;
074
075    /**
076     * builds the GUI
077     */
078    protected void build() {
079        WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(650,300));
080        geometry.applySafe(this);
081        getContentPane().setLayout(new BorderLayout());
082
083        model = new SaveLayersModel();
084        SaveLayersTable table = new SaveLayersTable(model);
085        JScrollPane pane = new JScrollPane(table);
086        model.addPropertyChangeListener(table);
087        table.getModel().addTableModelListener(this);
088
089        getContentPane().add(pane, BorderLayout.CENTER);
090        getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
091
092        addWindowListener(new WindowClosingAdapter());
093        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
094    }
095
096    private JButton saveAndProceedActionButton = null;
097
098    /**
099     * builds the button row
100     *
101     * @return the panel with the button row
102     */
103    protected JPanel buildButtonRow() {
104        JPanel pnl = new JPanel();
105        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
106
107        saveAndProceedAction = new SaveAndProceedAction();
108        model.addPropertyChangeListener(saveAndProceedAction);
109        pnl.add(saveAndProceedActionButton = new JButton(saveAndProceedAction));
110
111        discardAndProceedAction = new DiscardAndProceedAction();
112        model.addPropertyChangeListener(discardAndProceedAction);
113        pnl.add(new JButton(discardAndProceedAction));
114
115        cancelAction = new CancelAction();
116        pnl.add(new JButton(cancelAction));
117
118        JPanel pnl2 = new JPanel();
119        pnl2.setLayout(new BorderLayout());
120        pnl2.add(pnlUploadLayers = new UploadAndSaveProgressRenderer(), BorderLayout.CENTER);
121        model.addPropertyChangeListener(pnlUploadLayers);
122        pnl2.add(pnl, BorderLayout.SOUTH);
123        return pnl2;
124    }
125
126    public void prepareForSavingAndUpdatingLayersBeforeExit() {
127        setTitle(tr("Unsaved changes - Save/Upload before exiting?"));
128        this.saveAndProceedAction.initForSaveAndExit();
129        this.discardAndProceedAction.initForDiscardAndExit();
130    }
131
132    public void prepareForSavingAndUpdatingLayersBeforeDelete() {
133        setTitle(tr("Unsaved changes - Save/Upload before deleting?"));
134        this.saveAndProceedAction.initForSaveAndDelete();
135        this.discardAndProceedAction.initForDiscardAndDelete();
136    }
137
138    public SaveLayersDialog(Component parent) {
139        super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
140        build();
141    }
142
143    public UserAction getUserAction() {
144        return this.action;
145    }
146
147    public SaveLayersModel getModel() {
148        return model;
149    }
150
151    protected void launchSafeAndUploadTask() {
152        ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers);
153        monitor.beginTask(tr("Uploading and saving modified layers ..."));
154        this.saveAndUploadTask = new SaveAndUploadTask(model, monitor);
155        new Thread(saveAndUploadTask).start();
156    }
157
158    protected void cancelSafeAndUploadTask() {
159        if (this.saveAndUploadTask != null) {
160            this.saveAndUploadTask.cancel();
161        }
162        model.setMode(Mode.EDITING_DATA);
163    }
164
165    private static class  LayerListWarningMessagePanel extends JPanel {
166        private JLabel lblMessage;
167        private JList lstLayers;
168
169        protected void build() {
170            setLayout(new GridBagLayout());
171            GridBagConstraints gc = new GridBagConstraints();
172            gc.gridx = 0;
173            gc.gridy = 0;
174            gc.fill = GridBagConstraints.HORIZONTAL;
175            gc.weightx = 1.0;
176            gc.weighty = 0.0;
177            add(lblMessage = new JLabel(), gc);
178            lblMessage.setHorizontalAlignment(JLabel.LEFT);
179            lstLayers = new JList();
180            lstLayers.setCellRenderer(
181                    new DefaultListCellRenderer() {
182                        @Override
183                        public Component getListCellRendererComponent(JList list, Object value, int index,
184                                boolean isSelected, boolean cellHasFocus) {
185                            SaveLayerInfo info = (SaveLayerInfo)value;
186                            setIcon(info.getLayer().getIcon());
187                            setText(info.getName());
188                            return this;
189                        }
190                    }
191            );
192            gc.gridx = 0;
193            gc.gridy = 1;
194            gc.fill = GridBagConstraints.HORIZONTAL;
195            gc.weightx = 1.0;
196            gc.weighty = 1.0;
197            add(lstLayers,gc);
198        }
199
200        public LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) {
201            build();
202            lblMessage.setText(msg);
203            lstLayers.setListData(infos.toArray());
204        }
205    }
206
207    protected void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) {
208        String msg = trn("<html>{0} layer has unresolved conflicts.<br>"
209                + "Either resolve them first or discard the modifications.<br>"
210                + "Layer with conflicts:</html>",
211                "<html>{0} layers have unresolved conflicts.<br>"
212                + "Either resolve them first or discard the modifications.<br>"
213                + "Layers with conflicts:</html>",
214                infos.size(),
215                infos.size());
216        JOptionPane.showConfirmDialog(
217                Main.parent,
218                new LayerListWarningMessagePanel(msg, infos),
219                tr("Unsaved data and conflicts"),
220                JOptionPane.DEFAULT_OPTION,
221                JOptionPane.WARNING_MESSAGE
222        );
223    }
224
225    protected void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) {
226        String msg = trn("<html>{0} layer needs saving but has no associated file.<br>"
227                + "Either select a file for this layer or discard the changes.<br>"
228                + "Layer without a file:</html>",
229                "<html>{0} layers need saving but have no associated file.<br>"
230                + "Either select a file for each of them or discard the changes.<br>"
231                + "Layers without a file:</html>",
232                infos.size(),
233                infos.size());
234        JOptionPane.showConfirmDialog(
235                Main.parent,
236                new LayerListWarningMessagePanel(msg, infos),
237                tr("Unsaved data and missing associated file"),
238                JOptionPane.DEFAULT_OPTION,
239                JOptionPane.WARNING_MESSAGE
240        );
241    }
242
243    protected void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) {
244        String msg = trn("<html>{0} layer needs saving but has an associated file<br>"
245                + "which cannot be written.<br>"
246                + "Either select another file for this layer or discard the changes.<br>"
247                + "Layer with a non-writable file:</html>",
248                "<html>{0} layers need saving but have associated files<br>"
249                + "which cannot be written.<br>"
250                + "Either select another file for each of them or discard the changes.<br>"
251                + "Layers with non-writable files:</html>",
252                infos.size(),
253                infos.size());
254        JOptionPane.showConfirmDialog(
255                Main.parent,
256                new LayerListWarningMessagePanel(msg, infos),
257                tr("Unsaved data non-writable files"),
258                JOptionPane.DEFAULT_OPTION,
259                JOptionPane.WARNING_MESSAGE
260        );
261    }
262
263    protected boolean confirmSaveLayerInfosOK() {
264        List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest();
265        if (!layerInfos.isEmpty()) {
266            warnLayersWithConflictsAndUploadRequest(layerInfos);
267            return false;
268        }
269
270        layerInfos = model.getLayersWithoutFilesAndSaveRequest();
271        if (!layerInfos.isEmpty()) {
272            warnLayersWithoutFilesAndSaveRequest(layerInfos);
273            return false;
274        }
275
276        layerInfos = model.getLayersWithIllegalFilesAndSaveRequest();
277        if (!layerInfos.isEmpty()) {
278            warnLayersWithIllegalFilesAndSaveRequest(layerInfos);
279            return false;
280        }
281
282        return true;
283    }
284
285    protected void setUserAction(UserAction action) {
286        this.action = action;
287    }
288
289    public void closeDialog() {
290        setVisible(false);
291        dispose();
292    }
293
294    class WindowClosingAdapter extends WindowAdapter {
295        @Override
296        public void windowClosing(WindowEvent e) {
297            cancelAction.cancel();
298        }
299    }
300
301    class CancelAction extends AbstractAction {
302        public CancelAction() {
303            putValue(NAME, tr("Cancel"));
304            putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM"));
305            putValue(SMALL_ICON, ImageProvider.get("cancel"));
306            getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
307            .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
308            getRootPane().getActionMap().put("ESCAPE", this);
309        }
310
311        protected void cancelWhenInEditingModel() {
312            setUserAction(UserAction.CANCEL);
313            closeDialog();
314        }
315
316        protected void cancelWhenInSaveAndUploadingMode() {
317            cancelSafeAndUploadTask();
318        }
319
320        public void cancel() {
321            switch(model.getMode()) {
322            case EDITING_DATA: cancelWhenInEditingModel(); break;
323            case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); break;
324            }
325        }
326
327        @Override
328        public void actionPerformed(ActionEvent e) {
329            cancel();
330        }
331    }
332
333    class DiscardAndProceedAction extends AbstractAction  implements PropertyChangeListener {
334        public DiscardAndProceedAction() {
335            initForDiscardAndExit();
336        }
337
338        public void initForDiscardAndExit() {
339            putValue(NAME, tr("Exit now!"));
340            putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
341            putValue(SMALL_ICON, ImageProvider.get("exit"));
342        }
343
344        public void initForDiscardAndDelete() {
345            putValue(NAME, tr("Delete now!"));
346            putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
347            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
348        }
349
350        @Override
351        public void actionPerformed(ActionEvent e) {
352            setUserAction(UserAction.PROCEED);
353            closeDialog();
354        }
355        @Override
356        public void propertyChange(PropertyChangeEvent evt) {
357            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
358                Mode mode = (Mode)evt.getNewValue();
359                switch(mode) {
360                case EDITING_DATA: setEnabled(true); break;
361                case UPLOADING_AND_SAVING: setEnabled(false); break;
362                }
363            }
364        }
365    }
366
367    final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
368        private static final int is = 24; // icon size
369        private static final String BASE_ICON = "BASE_ICON";
370        private final Image save = ImageProvider.get("save").getImage();
371        private final Image upld = ImageProvider.get("upload").getImage();
372        private final Image saveDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
373        private final Image upldDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
374
375        public SaveAndProceedAction() {
376            // get disabled versions of icons
377            new JLabel(ImageProvider.get("save")).getDisabledIcon().paintIcon(new JPanel(), saveDis.getGraphics(), 0, 0);
378            new JLabel(ImageProvider.get("upload")).getDisabledIcon().paintIcon(new JPanel(), upldDis.getGraphics(), 0, 0);
379            initForSaveAndExit();
380        }
381
382        public void initForSaveAndExit() {
383            putValue(NAME, tr("Perform actions before exiting"));
384            putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
385            putValue(BASE_ICON, ImageProvider.get("exit"));
386            redrawIcon();
387        }
388
389        public void initForSaveAndDelete() {
390            putValue(NAME, tr("Perform actions before deleting"));
391            putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
392            putValue(BASE_ICON, ImageProvider.get("dialogs", "delete"));
393            redrawIcon();
394        }
395
396        public void redrawIcon() {
397            try { // Can fail if model is not yet setup properly
398                Image base = ((ImageIcon) getValue(BASE_ICON)).getImage();
399                BufferedImage newIco = new BufferedImage(is*3, is, BufferedImage.TYPE_4BYTE_ABGR);
400                Graphics2D g = newIco.createGraphics();
401                g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, is*0, 0, is, is, null);
402                g.drawImage(model.getLayersToSave().isEmpty()   ? saveDis : save, is*1, 0, is, is, null);
403                g.drawImage(base,                                                 is*2, 0, is, is, null);
404                putValue(SMALL_ICON, new ImageIcon(newIco));
405            } catch(Exception e) {
406                putValue(SMALL_ICON, getValue(BASE_ICON));
407            }
408        }
409
410        @Override
411        public void actionPerformed(ActionEvent e) {
412            if (! confirmSaveLayerInfosOK())
413                return;
414            launchSafeAndUploadTask();
415        }
416
417        @Override
418        public void propertyChange(PropertyChangeEvent evt) {
419            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
420                SaveLayersModel.Mode mode = (SaveLayersModel.Mode)evt.getNewValue();
421                switch(mode) {
422                case EDITING_DATA: setEnabled(true); break;
423                case UPLOADING_AND_SAVING: setEnabled(false); break;
424                }
425            }
426        }
427    }
428
429    /**
430     * This is the asynchronous task which uploads modified layers to the server and
431     * saves them to files, if requested by the user.
432     *
433     */
434    protected class SaveAndUploadTask implements Runnable {
435
436        private SaveLayersModel model;
437        private ProgressMonitor monitor;
438        private ExecutorService worker;
439        private boolean canceled;
440        private Future<?> currentFuture;
441        private AbstractIOTask currentTask;
442
443        public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
444            this.model = model;
445            this.monitor = monitor;
446            this.worker = Executors.newSingleThreadExecutor();
447        }
448
449        protected void uploadLayers(List<SaveLayerInfo> toUpload) {
450            for (final SaveLayerInfo layerInfo: toUpload) {
451                if (canceled) {
452                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
453                    continue;
454                }
455                monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
456
457                if (!new UploadAction().checkPreUploadConditions(layerInfo.getLayer())) {
458                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
459                    continue;
460                }
461                final UploadDialog dialog = UploadDialog.getUploadDialog();
462                dialog.setUploadedPrimitives(new APIDataSet(layerInfo.getLayer().data));
463                dialog.setVisible(true);
464                if (dialog.isCanceled()) {
465                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
466                    continue;
467                }
468                dialog.rememberUserInput();
469
470                currentTask = new UploadLayerTask(
471                        UploadDialog.getUploadDialog().getUploadStrategySpecification(),
472                        layerInfo.getLayer(),
473                        monitor,
474                        UploadDialog.getUploadDialog().getChangeset()
475                );
476                currentFuture = worker.submit(currentTask);
477                try {
478                    // wait for the asynchronous task to complete
479                    //
480                    currentFuture.get();
481                } catch(CancellationException e) {
482                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
483                } catch(Exception e) {
484                    e.printStackTrace();
485                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
486                    ExceptionDialogUtil.explainException(e);
487                }
488                if (currentTask.isCanceled()) {
489                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
490                } else if (currentTask.isFailed()) {
491                    currentTask.getLastException().printStackTrace();
492                    ExceptionDialogUtil.explainException(currentTask.getLastException());
493                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
494                } else {
495                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.OK);
496                }
497                currentTask = null;
498                currentFuture = null;
499            }
500        }
501
502        protected void saveLayers(List<SaveLayerInfo> toSave) {
503            for (final SaveLayerInfo layerInfo: toSave) {
504                if (canceled) {
505                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
506                    continue;
507                }
508                currentTask= new SaveLayerTask(layerInfo, monitor);
509                currentFuture = worker.submit(currentTask);
510
511                try {
512                    // wait for the asynchronous task to complete
513                    //
514                    currentFuture.get();
515                } catch(CancellationException e) {
516                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
517                } catch(Exception e) {
518                    e.printStackTrace();
519                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
520                    ExceptionDialogUtil.explainException(e);
521                }
522                if (currentTask.isCanceled()) {
523                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
524                } else if (currentTask.isFailed()) {
525                    if (currentTask.getLastException() != null) {
526                        currentTask.getLastException().printStackTrace();
527                        ExceptionDialogUtil.explainException(currentTask.getLastException());
528                    }
529                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
530                } else {
531                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
532                }
533                this.currentTask = null;
534                this.currentFuture = null;
535            }
536        }
537
538        protected void warnBecauseOfUnsavedData() {
539            int numProblems = model.getNumCancel() + model.getNumFailed();
540            if (numProblems == 0) return;
541            String msg = trn(
542                    "<html>An upload and/or save operation of one layer with modifications<br>"
543                    + "was canceled or has failed.</html>",
544                    "<html>Upload and/or save operations of {0} layers with modifications<br>"
545                    + "were canceled or have failed.</html>",
546                    numProblems,
547                    numProblems
548            );
549            JOptionPane.showMessageDialog(
550                    Main.parent,
551                    msg,
552                    tr("Incomplete upload and/or save"),
553                    JOptionPane.WARNING_MESSAGE
554            );
555        }
556
557        @Override
558        public void run() {
559            model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
560            List<SaveLayerInfo> toUpload = model.getLayersToUpload();
561            if (!toUpload.isEmpty()) {
562                uploadLayers(toUpload);
563            }
564            List<SaveLayerInfo> toSave = model.getLayersToSave();
565            if (!toSave.isEmpty()) {
566                saveLayers(toSave);
567            }
568            model.setMode(SaveLayersModel.Mode.EDITING_DATA);
569            if (model.hasUnsavedData()) {
570                warnBecauseOfUnsavedData();
571                model.setMode(Mode.EDITING_DATA);
572                if (canceled) {
573                    setUserAction(UserAction.CANCEL);
574                    closeDialog();
575                }
576            } else {
577                setUserAction(UserAction.PROCEED);
578                closeDialog();
579            }
580        }
581
582        public void cancel() {
583            if (currentTask != null) {
584                currentTask.cancel();
585            }
586            canceled = true;
587        }
588    }
589
590    @Override
591    public void tableChanged(TableModelEvent arg0) {
592        boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
593        if(saveAndProceedActionButton != null) {
594            saveAndProceedActionButton.setEnabled(!dis);
595        }
596        saveAndProceedAction.redrawIcon();
597    }
598}