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}