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 }