001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.io;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trn;
006    
007    import java.awt.BorderLayout;
008    import java.awt.Component;
009    import java.awt.Dimension;
010    import java.awt.FlowLayout;
011    import java.awt.Graphics2D;
012    import java.awt.GridBagConstraints;
013    import java.awt.GridBagLayout;
014    import java.awt.Image;
015    import java.awt.event.ActionEvent;
016    import java.awt.event.WindowAdapter;
017    import java.awt.event.WindowEvent;
018    import java.awt.image.BufferedImage;
019    import java.beans.PropertyChangeEvent;
020    import java.beans.PropertyChangeListener;
021    import java.util.List;
022    import java.util.concurrent.CancellationException;
023    import java.util.concurrent.ExecutorService;
024    import java.util.concurrent.Executors;
025    import java.util.concurrent.Future;
026    
027    import javax.swing.AbstractAction;
028    import javax.swing.DefaultListCellRenderer;
029    import javax.swing.ImageIcon;
030    import javax.swing.JComponent;
031    import javax.swing.JButton;
032    import javax.swing.JDialog;
033    import javax.swing.JLabel;
034    import javax.swing.JList;
035    import javax.swing.JOptionPane;
036    import javax.swing.JPanel;
037    import javax.swing.JScrollPane;
038    import javax.swing.KeyStroke;
039    import javax.swing.WindowConstants;
040    import javax.swing.event.TableModelEvent;
041    import javax.swing.event.TableModelListener;
042    
043    import org.openstreetmap.josm.Main;
044    import org.openstreetmap.josm.actions.UploadAction;
045    import org.openstreetmap.josm.data.APIDataSet;
046    import org.openstreetmap.josm.gui.ExceptionDialogUtil;
047    import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
048    import org.openstreetmap.josm.gui.progress.ProgressMonitor;
049    import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
050    import org.openstreetmap.josm.tools.ImageProvider;
051    import org.openstreetmap.josm.tools.WindowGeometry;
052    
053    public 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            public void actionPerformed(ActionEvent e) {
328                cancel();
329            }
330        }
331    
332        class DiscardAndProceedAction extends AbstractAction  implements PropertyChangeListener {
333            public DiscardAndProceedAction() {
334                initForDiscardAndExit();
335            }
336    
337            public void initForDiscardAndExit() {
338                putValue(NAME, tr("Exit now!"));
339                putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
340                putValue(SMALL_ICON, ImageProvider.get("exit"));
341            }
342    
343            public void initForDiscardAndDelete() {
344                putValue(NAME, tr("Delete now!"));
345                putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
346                putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
347            }
348    
349            public void actionPerformed(ActionEvent e) {
350                setUserAction(UserAction.PROCEED);
351                closeDialog();
352            }
353            public void propertyChange(PropertyChangeEvent evt) {
354                if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
355                    Mode mode = (Mode)evt.getNewValue();
356                    switch(mode) {
357                    case EDITING_DATA: setEnabled(true); break;
358                    case UPLOADING_AND_SAVING: setEnabled(false); break;
359                    }
360                }
361            }
362        }
363    
364        final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
365            private static final int is = 24; // icon size
366            private static final String BASE_ICON = "BASE_ICON";
367            private final Image save = ImageProvider.get("save").getImage();
368            private final Image upld = ImageProvider.get("upload").getImage();
369            private final Image saveDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
370            private final Image upldDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR);
371    
372            public SaveAndProceedAction() {
373                // get disabled versions of icons
374                new JLabel(ImageProvider.get("save")).getDisabledIcon().paintIcon(new JPanel(), saveDis.getGraphics(), 0, 0);
375                new JLabel(ImageProvider.get("upload")).getDisabledIcon().paintIcon(new JPanel(), upldDis.getGraphics(), 0, 0);
376                initForSaveAndExit();
377            }
378    
379            public void initForSaveAndExit() {
380                putValue(NAME, tr("Perform actions before exiting"));
381                putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
382                putValue(BASE_ICON, ImageProvider.get("exit"));
383                redrawIcon();
384            }
385    
386            public void initForSaveAndDelete() {
387                putValue(NAME, tr("Perform actions before deleting"));
388                putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
389                putValue(BASE_ICON, ImageProvider.get("dialogs", "delete"));
390                redrawIcon();
391            }
392    
393            public void redrawIcon() {
394                try { // Can fail if model is not yet setup properly
395                    Image base = ((ImageIcon) getValue(BASE_ICON)).getImage();
396                    BufferedImage newIco = new BufferedImage(is*3, is, BufferedImage.TYPE_4BYTE_ABGR);
397                    Graphics2D g = newIco.createGraphics();
398                    g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, is*0, 0, is, is, null);
399                    g.drawImage(model.getLayersToSave().isEmpty()   ? saveDis : save, is*1, 0, is, is, null);
400                    g.drawImage(base,                                                 is*2, 0, is, is, null);
401                    putValue(SMALL_ICON, new ImageIcon(newIco));
402                } catch(Exception e) {
403                    putValue(SMALL_ICON, getValue(BASE_ICON));
404                }
405            }
406    
407            public void actionPerformed(ActionEvent e) {
408                if (! confirmSaveLayerInfosOK())
409                    return;
410                launchSafeAndUploadTask();
411            }
412    
413            public void propertyChange(PropertyChangeEvent evt) {
414                if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
415                    SaveLayersModel.Mode mode = (SaveLayersModel.Mode)evt.getNewValue();
416                    switch(mode) {
417                    case EDITING_DATA: setEnabled(true); break;
418                    case UPLOADING_AND_SAVING: setEnabled(false); break;
419                    }
420                }
421            }
422        }
423    
424        /**
425         * This is the asynchronous task which uploads modified layers to the server and
426         * saves them to files, if requested by the user.
427         *
428         */
429        protected class SaveAndUploadTask implements Runnable {
430    
431            private SaveLayersModel model;
432            private ProgressMonitor monitor;
433            private ExecutorService worker;
434            private boolean canceled;
435            private Future<?> currentFuture;
436            private AbstractIOTask currentTask;
437    
438            public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
439                this.model = model;
440                this.monitor = monitor;
441                this.worker = Executors.newSingleThreadExecutor();
442            }
443    
444            protected void uploadLayers(List<SaveLayerInfo> toUpload) {
445                for (final SaveLayerInfo layerInfo: toUpload) {
446                    if (canceled) {
447                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
448                        continue;
449                    }
450                    monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
451    
452                    if (!new UploadAction().checkPreUploadConditions(layerInfo.getLayer())) {
453                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
454                        continue;
455                    }
456                    final UploadDialog dialog = UploadDialog.getUploadDialog();
457                    dialog.setUploadedPrimitives(new APIDataSet(layerInfo.getLayer().data));
458                    dialog.setVisible(true);
459                    if (dialog.isCanceled()) {
460                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
461                        continue;
462                    }
463                    dialog.rememberUserInput();
464    
465                    currentTask = new UploadLayerTask(
466                            UploadDialog.getUploadDialog().getUploadStrategySpecification(),
467                            layerInfo.getLayer(),
468                            monitor,
469                            UploadDialog.getUploadDialog().getChangeset()
470                    );
471                    currentFuture = worker.submit(currentTask);
472                    try {
473                        // wait for the asynchronous task to complete
474                        //
475                        currentFuture.get();
476                    } catch(CancellationException e) {
477                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
478                    } catch(Exception e) {
479                        e.printStackTrace();
480                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
481                        ExceptionDialogUtil.explainException(e);
482                    }
483                    if (currentTask.isCanceled()) {
484                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
485                    } else if (currentTask.isFailed()) {
486                        currentTask.getLastException().printStackTrace();
487                        ExceptionDialogUtil.explainException(currentTask.getLastException());
488                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
489                    } else {
490                        model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.OK);
491                    }
492                    currentTask = null;
493                    currentFuture = null;
494                }
495            }
496    
497            protected void saveLayers(List<SaveLayerInfo> toSave) {
498                for (final SaveLayerInfo layerInfo: toSave) {
499                    if (canceled) {
500                        model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
501                        continue;
502                    }
503                    currentTask= new SaveLayerTask(layerInfo, monitor);
504                    currentFuture = worker.submit(currentTask);
505    
506                    try {
507                        // wait for the asynchronous task to complete
508                        //
509                        currentFuture.get();
510                    } catch(CancellationException e) {
511                        model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
512                    } catch(Exception e) {
513                        e.printStackTrace();
514                        model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
515                        ExceptionDialogUtil.explainException(e);
516                    }
517                    if (currentTask.isCanceled()) {
518                        model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
519                    } else if (currentTask.isFailed()) {
520                        if (currentTask.getLastException() != null) {
521                            currentTask.getLastException().printStackTrace();
522                            ExceptionDialogUtil.explainException(currentTask.getLastException());
523                        }
524                        model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
525                    } else {
526                        model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
527                    }
528                    this.currentTask = null;
529                    this.currentFuture = null;
530                }
531            }
532    
533            protected void warnBecauseOfUnsavedData() {
534                int numProblems = model.getNumCancel() + model.getNumFailed();
535                if (numProblems == 0) return;
536                String msg = trn(
537                        "<html>An upload and/or save operation of one layer with modifications<br>"
538                        + "was canceled or has failed.</html>",
539                        "<html>Upload and/or save operations of {0} layers with modifications<br>"
540                        + "were canceled or have failed.</html>",
541                        numProblems,
542                        numProblems
543                );
544                JOptionPane.showMessageDialog(
545                        Main.parent,
546                        msg,
547                        tr("Incomplete upload and/or save"),
548                        JOptionPane.WARNING_MESSAGE
549                );
550            }
551    
552            public void run() {
553                model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
554                List<SaveLayerInfo> toUpload = model.getLayersToUpload();
555                if (!toUpload.isEmpty()) {
556                    uploadLayers(toUpload);
557                }
558                List<SaveLayerInfo> toSave = model.getLayersToSave();
559                if (!toSave.isEmpty()) {
560                    saveLayers(toSave);
561                }
562                model.setMode(SaveLayersModel.Mode.EDITING_DATA);
563                if (model.hasUnsavedData()) {
564                    warnBecauseOfUnsavedData();
565                    model.setMode(Mode.EDITING_DATA);
566                    if (canceled) {
567                        setUserAction(UserAction.CANCEL);
568                        closeDialog();
569                    }
570                } else {
571                    setUserAction(UserAction.PROCEED);
572                    closeDialog();
573                }
574            }
575    
576            public void cancel() {
577                if (currentTask != null) {
578                    currentTask.cancel();
579                }
580                canceled = true;
581            }
582        }
583    
584        @Override
585        public void tableChanged(TableModelEvent arg0) {
586            boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
587            if(saveAndProceedActionButton != null) {
588                saveAndProceedActionButton.setEnabled(!dis);
589            }
590            saveAndProceedAction.redrawIcon();
591        }
592    }