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 }