source: josm/trunk/src/org/openstreetmap/josm/actions/UploadAction.java@ 2061

Last change on this file since 2061 was 2061, checked in by Gubaer, 15 years ago

fixed #3398: Changeset tags should be editable in subsequent uploads

  • Property svn:eol-style set to native
File size: 38.1 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Dimension;
9import java.awt.GridBagConstraints;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.ActionListener;
13import java.awt.event.KeyEvent;
14import java.io.IOException;
15import java.net.HttpURLConnection;
16import java.util.Collection;
17import java.util.HashMap;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.logging.Logger;
22import java.util.regex.Matcher;
23import java.util.regex.Pattern;
24
25import javax.swing.BoxLayout;
26import javax.swing.ButtonGroup;
27import javax.swing.JCheckBox;
28import javax.swing.JLabel;
29import javax.swing.JList;
30import javax.swing.JOptionPane;
31import javax.swing.JPanel;
32import javax.swing.JRadioButton;
33import javax.swing.JScrollPane;
34import javax.swing.JTabbedPane;
35
36import org.openstreetmap.josm.Main;
37import org.openstreetmap.josm.data.APIDataSet;
38import org.openstreetmap.josm.data.conflict.ConflictCollection;
39import org.openstreetmap.josm.data.osm.Changeset;
40import org.openstreetmap.josm.data.osm.DataSet;
41import org.openstreetmap.josm.data.osm.OsmPrimitive;
42import org.openstreetmap.josm.gui.ExceptionDialogUtil;
43import org.openstreetmap.josm.gui.ExtendedDialog;
44import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
45import org.openstreetmap.josm.gui.PleaseWaitRunnable;
46import org.openstreetmap.josm.gui.historycombobox.SuggestingJHistoryComboBox;
47import org.openstreetmap.josm.gui.layer.OsmDataLayer;
48import org.openstreetmap.josm.gui.progress.ProgressMonitor;
49import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
50import org.openstreetmap.josm.io.ChangesetProcessingType;
51import org.openstreetmap.josm.io.OsmApi;
52import org.openstreetmap.josm.io.OsmApiException;
53import org.openstreetmap.josm.io.OsmApiInitializationException;
54import org.openstreetmap.josm.io.OsmChangesetCloseException;
55import org.openstreetmap.josm.io.OsmServerWriter;
56import org.openstreetmap.josm.tools.GBC;
57import org.openstreetmap.josm.tools.Shortcut;
58import org.openstreetmap.josm.tools.WindowGeometry;
59import org.xml.sax.SAXException;
60
61
62/**
63 * Action that opens a connection to the osm server and uploads all changes.
64 *
65 * An dialog is displayed asking the user to specify a rectangle to grab.
66 * The url and account settings from the preferences are used.
67 *
68 * If the upload fails this action offers various options to resolve conflicts.
69 *
70 * @author imi
71 */
72public class UploadAction extends JosmAction{
73 static private Logger logger = Logger.getLogger(UploadAction.class.getName());
74
75 public static final String HISTORY_KEY = "upload.comment.history";
76
77 /** Upload Hook */
78 public interface UploadHook {
79 /**
80 * Checks the upload.
81 * @param add The added primitives
82 * @param update The updated primitives
83 * @param delete The deleted primitives
84 * @return true, if the upload can continue
85 */
86 public boolean checkUpload(Collection<OsmPrimitive> add, Collection<OsmPrimitive> update, Collection<OsmPrimitive> delete);
87 }
88
89 /**
90 * The list of upload hooks. These hooks will be called one after the other
91 * when the user wants to upload data. Plugins can insert their own hooks here
92 * if they want to be able to veto an upload.
93 *
94 * Be default, the standard upload dialog is the only element in the list.
95 * Plugins should normally insert their code before that, so that the upload
96 * dialog is the last thing shown before upload really starts; on occasion
97 * however, a plugin might also want to insert something after that.
98 */
99 public final LinkedList<UploadHook> uploadHooks = new LinkedList<UploadHook>();
100
101 public UploadAction() {
102 super(tr("Upload to OSM..."), "upload", tr("Upload all changes to the OSM server."),
103 Shortcut.registerShortcut("file:upload", tr("File: {0}", tr("Upload to OSM...")), KeyEvent.VK_U, Shortcut.GROUPS_ALT1+Shortcut.GROUP_HOTKEY), true);
104
105 /**
106 * Checks server capabilities before upload.
107 */
108 uploadHooks.add(new ApiPreconditionChecker());
109
110 /**
111 * Displays a screen where the actions that would be taken are displayed and
112 * give the user the possibility to cancel the upload.
113 */
114 uploadHooks.add(new UploadConfirmationHook());
115 }
116
117 /**
118 * Refreshes the enabled state
119 *
120 */
121 @Override
122 protected void updateEnabledState() {
123 setEnabled(getEditLayer() != null);
124 }
125
126 public boolean checkPreUploadConditions(OsmDataLayer layer) {
127 return checkPreUploadConditions(layer, new APIDataSet(layer.data));
128 }
129
130 public boolean checkPreUploadConditions(OsmDataLayer layer, APIDataSet apiData) {
131 ConflictCollection conflicts = layer.getConflicts();
132 if (conflicts !=null && !conflicts.isEmpty()) {
133 JOptionPane.showMessageDialog(
134 Main.parent,
135 tr("<html>There are unresolved conflicts in layer ''{0}''.<br>"
136 + "You have to resolve them first.<html>", layer.getName()),
137 tr("Warning"),
138 JOptionPane.WARNING_MESSAGE
139 );
140 return false;
141 }
142 // Call all upload hooks in sequence. The upload confirmation dialog
143 // is one of these.
144 for(UploadHook hook : uploadHooks)
145 if(!hook.checkUpload(apiData.getPrimitivesToAdd(), apiData.getPrimitivesToUpdate(), apiData.getPrimitivesToDelete()))
146 return false;
147
148 return true;
149 }
150
151 public void actionPerformed(ActionEvent e) {
152 if (!isEnabled())
153 return;
154 if (Main.map == null) {
155 JOptionPane.showMessageDialog(
156 Main.parent,
157 tr("Nothing to upload. Get some data first."),
158 tr("Warning"),
159 JOptionPane.WARNING_MESSAGE
160 );
161 return;
162 }
163
164 APIDataSet apiData = new APIDataSet(Main.main.getCurrentDataSet());
165 if (apiData.isEmpty()) {
166 JOptionPane.showMessageDialog(
167 Main.parent,
168 tr("No changes to upload."),
169 tr("Warning"),
170 JOptionPane.INFORMATION_MESSAGE
171 );
172 return;
173 }
174 if (!checkPreUploadConditions(Main.map.mapView.getEditLayer(), apiData))
175 return;
176 Main.worker.execute(
177 createUploadTask(
178 Main.map.mapView.getEditLayer(),
179 apiData.getPrimitives(),
180 UploadConfirmationHook.getUploadDialogPanel().getChangeset(),
181 UploadConfirmationHook.getUploadDialogPanel().getChangesetProcessingType()
182 )
183 );
184 }
185
186 /**
187 * Synchronizes the local state of an {@see OsmPrimitive} with its state on the
188 * server. The method uses an individual GET for the primitive.
189 *
190 * @param id the primitive ID
191 */
192 protected void synchronizePrimitive(final String id) {
193 Main.worker.execute(new UpdatePrimitiveTask(Long.parseLong(id)));
194 }
195
196 /**
197 * Synchronizes the local state of the dataset with the state on the server.
198 *
199 * Reuses the functionality of {@see UpdateDataAction}.
200 *
201 * @see UpdateDataAction#actionPerformed(ActionEvent)
202 */
203 protected void synchronizeDataSet() {
204 UpdateDataAction act = new UpdateDataAction();
205 act.actionPerformed(new ActionEvent(this,0,""));
206 }
207
208 /**
209 * Handles the case that a conflict in a specific {@see OsmPrimitive} was detected while
210 * uploading
211 *
212 * @param primitiveType the type of the primitive, either <code>node</code>, <code>way</code> or
213 * <code>relation</code>
214 * @param id the id of the primitive
215 * @param serverVersion the version of the primitive on the server
216 * @param myVersion the version of the primitive in the local dataset
217 */
218 protected void handleUploadConflictForKnownConflict(String primitiveType, String id, String serverVersion, String myVersion) {
219 Object[] options = new Object[] {
220 tr("Synchronize {0} {1} only", tr(primitiveType), id),
221 tr("Synchronize entire dataset"),
222 tr("Cancel")
223 };
224 Object defaultOption = options[0];
225 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
226 + "of your nodes, ways, or relations.<br>"
227 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>"
228 + "the server has version {2}, your version is {3}.<br>"
229 + "<br>"
230 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>"
231 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>"
232 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>",
233 tr(primitiveType), id, serverVersion, myVersion,
234 options[0], options[1], options[2]
235 );
236 int optionsType = JOptionPane.YES_NO_CANCEL_OPTION;
237 int ret = JOptionPane.showOptionDialog(
238 null,
239 msg,
240 tr("Conflict detected"),
241 optionsType,
242 JOptionPane.ERROR_MESSAGE,
243 null,
244 options,
245 defaultOption
246 );
247 switch(ret) {
248 case JOptionPane.CLOSED_OPTION: return;
249 case JOptionPane.CANCEL_OPTION: return;
250 case 0: synchronizePrimitive(id); break;
251 case 1: synchronizeDataSet(); break;
252 default:
253 // should not happen
254 throw new IllegalStateException(tr("unexpected return value. Got {0}", ret));
255 }
256 }
257
258 /**
259 * Handles the case that a conflict was detected while uploading where we don't
260 * know what {@see OsmPrimitive} actually caused the conflict (for whatever reason)
261 *
262 */
263 protected void handleUploadConflictForUnknownConflict() {
264 Object[] options = new Object[] {
265 tr("Synchronize entire dataset"),
266 tr("Cancel")
267 };
268 Object defaultOption = options[0];
269 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
270 + "of your nodes, ways, or relations.<br>"
271 + "<br>"
272 + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>"
273 + "Click <strong>{1}</strong> to abort and continue editing.<br></html>",
274 options[0], options[1]
275 );
276 int optionsType = JOptionPane.YES_NO_OPTION;
277 int ret = JOptionPane.showOptionDialog(
278 null,
279 msg,
280 tr("Conflict detected"),
281 optionsType,
282 JOptionPane.ERROR_MESSAGE,
283 null,
284 options,
285 defaultOption
286 );
287 switch(ret) {
288 case JOptionPane.CLOSED_OPTION: return;
289 case 1: return;
290 case 0: synchronizeDataSet(); break;
291 default:
292 // should not happen
293 throw new IllegalStateException(tr("unexpected return value. Got {0}", ret));
294 }
295 }
296
297 /**
298 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
299 *
300 * @param e the exception
301 */
302 protected void handleUploadConflict(OsmApiException e) {
303 String pattern = "Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)";
304 Pattern p = Pattern.compile(pattern);
305 Matcher m = p.matcher(e.getErrorHeader());
306 if (m.matches()) {
307 handleUploadConflictForKnownConflict(m.group(3), m.group(4), m.group(2),m.group(1));
308 } else {
309 logger.warning(tr("Warning: error header \"{0}\" did not match expected pattern \"{1}\"", e.getErrorHeader(),pattern));
310 handleUploadConflictForUnknownConflict();
311 }
312 }
313
314 /**
315 * Handles an error due to a delete request on an already deleted
316 * {@see OsmPrimitive}, i.e. a HTTP response code 410, where we know what
317 * {@see OsmPrimitive} is responsible for the error.
318 *
319 * Reuses functionality of the {@see UpdateSelectionAction} to resolve
320 * conflicts due to mismatches in the deleted state.
321 *
322 * @param primitiveType the type of the primitive
323 * @param id the id of the primitive
324 *
325 * @see UpdateSelectionAction#handlePrimitiveGoneException(long)
326 */
327 protected void handleGoneForKnownPrimitive(String primitiveType, String id) {
328 UpdateSelectionAction act = new UpdateSelectionAction();
329 act.handlePrimitiveGoneException(Long.parseLong(id));
330 }
331
332 /**
333 * Handles an error which is caused by a delete request for an already deleted
334 * {@see OsmPrimitive} on the server, i.e. a HTTP response code of 410.
335 * Note that an <strong>update</strong> on an already deleted object results
336 * in a 409, not a 410.
337 *
338 * @param e the exception
339 */
340 protected void handleGone(OsmApiException e) {
341 String pattern = "The (\\S+) with the id (\\d+) has already been deleted";
342 Pattern p = Pattern.compile(pattern);
343 Matcher m = p.matcher(e.getErrorHeader());
344 if (m.matches()) {
345 handleGoneForKnownPrimitive(m.group(1), m.group(2));
346 } else {
347 logger.warning(tr("Error header \"{0}\" does not match expected pattern \"{1}\"",e.getErrorHeader(), pattern));
348 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
349 }
350 }
351
352
353 /**
354 * error handler for any exception thrown during upload
355 *
356 * @param e the exception
357 */
358 protected void handleFailedUpload(Exception e) {
359 // API initialization failed. Notify the user and return.
360 //
361 if (e instanceof OsmApiInitializationException) {
362 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException)e);
363 return;
364 }
365
366 if (e instanceof OsmChangesetCloseException) {
367 ExceptionDialogUtil.explainOsmChangesetCloseException((OsmChangesetCloseException)e);
368 return;
369 }
370 if (e instanceof OsmApiException) {
371 OsmApiException ex = (OsmApiException)e;
372 // There was an upload conflict. Let the user decide whether
373 // and how to resolve it
374 //
375 if(ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
376 handleUploadConflict(ex);
377 return;
378 }
379 // There was a precondition failed. Notify the user.
380 //
381 else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
382 ExceptionDialogUtil.explainPreconditionFailed(ex);
383 return;
384 }
385 // Tried to delete an already deleted primitive? Let the user
386 // decide whether and how to resolve this conflict.
387 //
388 else if (ex.getResponseCode() == HttpURLConnection.HTTP_GONE) {
389 handleGone(ex);
390 return;
391 }
392 // any other API exception
393 //
394 else {
395 ex.printStackTrace();
396 String msg = tr("<html>Uploading <strong>failed</strong>."
397 + "<br>"
398 + "{0}"
399 + "</html>",
400 ex.getDisplayMessage()
401 );
402 JOptionPane.showMessageDialog(
403 Main.map,
404 msg,
405 tr("Upload to OSM API failed"),
406 JOptionPane.ERROR_MESSAGE
407 );
408 return;
409 }
410 }
411
412 ExceptionDialogUtil.explainException(e);
413 }
414
415 /**
416 * The asynchronous task to update a specific id
417 *
418 */
419 class UpdatePrimitiveTask extends PleaseWaitRunnable {
420
421 private boolean uploadCancelled = false;
422 private boolean uploadFailed = false;
423 private Exception lastException = null;
424 private long id;
425
426 public UpdatePrimitiveTask(long id) {
427 super(tr("Updating primitive"),false /* don't ignore exceptions */);
428 this.id = id;
429 }
430
431 @Override protected void realRun() throws SAXException, IOException {
432 try {
433 UpdateSelectionAction act = new UpdateSelectionAction();
434 act.updatePrimitive(id);
435 } catch (Exception sxe) {
436 if (uploadCancelled) {
437 System.out.println("Ignoring exception caught because upload is cancelled. Exception is: " + sxe.toString());
438 return;
439 }
440 uploadFailed = true;
441 lastException = sxe;
442 }
443 }
444
445 @Override protected void finish() {
446 if (uploadFailed) {
447 handleFailedUpload(lastException);
448 }
449 }
450
451 @Override protected void cancel() {
452 OsmApi.getOsmApi().cancel();
453 uploadCancelled = true;
454 }
455 }
456
457
458 static public class UploadConfirmationHook implements UploadHook {
459 static private UploadDialogPanel uploadDialogPanel;
460
461 static public UploadDialogPanel getUploadDialogPanel() {
462 if (uploadDialogPanel == null) {
463 uploadDialogPanel = new UploadDialogPanel();
464 }
465 return uploadDialogPanel;
466 }
467
468 public boolean checkUpload(Collection<OsmPrimitive> add, Collection<OsmPrimitive> update, Collection<OsmPrimitive> delete) {
469 final UploadDialogPanel panel = getUploadDialogPanel();
470 panel.setUploadedPrimitives(add, update, delete);
471
472 ExtendedDialog dialog = new ExtendedDialog(
473 Main.parent,
474 tr("Upload these changes?"),
475 new String[] {tr("Upload Changes"), tr("Cancel")}
476 ) {
477 @Override
478 public void setVisible(boolean visible) {
479 if (visible) {
480 new WindowGeometry(
481 panel.getClass().getName(),
482 WindowGeometry.centerInWindow(JOptionPane.getFrameForComponent(Main.parent), new Dimension(400,400))
483 ).apply(this);
484 panel.startUserInput();
485 } else {
486 new WindowGeometry(this).remember(panel.getClass().getName());
487 }
488 super.setVisible(visible);
489 }
490 };
491
492 dialog.setButtonIcons(new String[] {"upload.png", "cancel.png"});
493 dialog.setContent(panel, false /* no scroll pane */);
494 while(true) {
495 dialog.showDialog();
496 int result = dialog.getValue();
497 // cancel pressed
498 if (result != 1) return false;
499 // don't allow empty commit message
500 if (! panel.hasChangesetComment()) {
501 continue;
502 }
503 panel.rememberUserInput();
504 break;
505 }
506 return true;
507 }
508 }
509
510 public UploadDiffTask createUploadTask(OsmDataLayer layer, Collection<OsmPrimitive> toUpload, Changeset changeset, ChangesetProcessingType changesetProcessingType) {
511 return new UploadDiffTask(layer, toUpload, changeset, changesetProcessingType);
512 }
513
514 public class UploadDiffTask extends PleaseWaitRunnable {
515 private boolean uploadCancelled = false;
516 private Exception lastException = null;
517 private Collection <OsmPrimitive> toUpload;
518 private OsmServerWriter writer;
519 private OsmDataLayer layer;
520 private Changeset changeset;
521 private ChangesetProcessingType changesetProcessingType;
522
523 private UploadDiffTask(OsmDataLayer layer, Collection <OsmPrimitive> toUpload, Changeset changeset, ChangesetProcessingType changesetProcessingType) {
524 super(tr("Uploading data for layer ''{0}''", layer.getName()),false /* don't ignore exceptions */);
525 this.toUpload = toUpload;
526 this.layer = layer;
527 this.changeset = changeset;
528 this.changesetProcessingType = changesetProcessingType == null ? ChangesetProcessingType.USE_NEW_AND_CLOSE : changesetProcessingType;
529 }
530
531 @Override protected void realRun() throws SAXException, IOException {
532 writer = new OsmServerWriter();
533 try {
534 ProgressMonitor monitor = progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
535 writer.uploadOsm(layer.data.version, toUpload, changeset,changesetProcessingType, monitor);
536 } catch (Exception sxe) {
537 if (uploadCancelled) {
538 System.out.println("Ignoring exception caught because upload is cancelled. Exception is: " + sxe.toString());
539 return;
540 }
541 lastException = sxe;
542 }
543 }
544
545 @Override protected void finish() {
546 if (uploadCancelled)
547 return;
548
549 // we always clean the data, even in case of errors. It's possible the data was
550 // partially uploaded
551 //
552 layer.cleanupAfterUpload(writer.getProcessedPrimitives());
553 DataSet.fireSelectionChanged(layer.data.getSelected());
554 layer.fireDataChange();
555 if (lastException != null) {
556 handleFailedUpload(lastException);
557 } else {
558 layer.onPostUploadToServer();
559 }
560 }
561
562 @Override protected void cancel() {
563 uploadCancelled = true;
564 if (writer != null) {
565 writer.cancel();
566 }
567 }
568
569 public boolean isSuccessful() {
570 return !isCancelled() && !isFailed();
571 }
572
573 public boolean isCancelled() {
574 return uploadCancelled;
575 }
576
577 public boolean isFailed() {
578 return lastException != null;
579 }
580 }
581
582 /**
583 * The panel displaying information about primitives to upload and providing
584 * UI widgets for entering the changeset comment and other configuration
585 * settings.
586 *
587 */
588 static public class UploadDialogPanel extends JPanel {
589
590 /** the list with the added primitives */
591 private JList lstAdd;
592 private JLabel lblAdd;
593 private JScrollPane spAdd;
594 /** the list with the updated primitives */
595 private JList lstUpdate;
596 private JLabel lblUpdate;
597 private JScrollPane spUpdate;
598 /** the list with the deleted primitives */
599 private JList lstDelete;
600 private JLabel lblDelete;
601 private JScrollPane spDelete;
602 /** the panel containing the widgets for the lists of primitives */
603 private JPanel pnlLists;
604 /** checkbox for selecting whether an atomic upload is to be used */
605 private JCheckBox cbUseAtomicUpload;
606 /** input field for changeset comment */
607 private SuggestingJHistoryComboBox cmt;
608 /** ui component for editing changeset tags */
609 private TagEditorPanel tagEditorPanel;
610 /** the tabbed pane used below of the list of primitives */
611 private JTabbedPane southTabbedPane;
612 /** the button group with the changeset processing types */
613 private ButtonGroup bgChangesetHandlingOptions;
614 /** radio buttons for selecting a changeset processing type */
615 private Map<ChangesetProcessingType, JRadioButton> rbChangesetHandlingOptions;
616
617 /**
618 * builds the panel with the lists of primitives
619 *
620 * @return the panel with the lists of primitives
621 */
622 protected JPanel buildListsPanel() {
623 pnlLists = new JPanel();
624 pnlLists.setLayout(new GridBagLayout());
625 // we don't add the lists yet, see setUploadPrimivies()
626 //
627 return pnlLists;
628 }
629
630 /**
631 * builds the panel with the ui components for controlling how the changeset
632 * should be processed (opening/closing a changeset)
633 *
634 * @return the panel with the ui components for controlling how the changeset
635 * should be processed
636 */
637 protected JPanel buildChangesetHandlingControlPanel() {
638 JPanel pnl = new JPanel();
639 pnl.setLayout(new BoxLayout(pnl, BoxLayout.Y_AXIS));
640 bgChangesetHandlingOptions = new ButtonGroup();
641 rbChangesetHandlingOptions = new HashMap<ChangesetProcessingType, JRadioButton>();
642 ChangesetProcessingTypeChangedAction a = new ChangesetProcessingTypeChangedAction();
643 for(ChangesetProcessingType type: ChangesetProcessingType.values()) {
644 rbChangesetHandlingOptions.put(type, new JRadioButton());
645 rbChangesetHandlingOptions.get(type).addActionListener(a);
646 }
647 JRadioButton rb = rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_NEW_AND_CLOSE);
648 rb.setText(tr("Use a new changeset and close it"));
649 rb.setToolTipText(tr("Select to upload the data using a new changeset and to close the changeset after the upload"));
650
651 rb = rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_NEW_AND_LEAVE_OPEN);
652 rb.setText(tr("Use a new changeset and leave it open"));
653 rb.setToolTipText(tr("Select to upload the data using a new changeset and to leave the changeset open after the upload"));
654
655 pnl.add(new JLabel(tr("Upload to a new or to an existing changeset?")));
656 pnl.add(rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_NEW_AND_CLOSE));
657 pnl.add(rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_NEW_AND_LEAVE_OPEN));
658 pnl.add(rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_EXISTING_AND_CLOSE));
659 pnl.add(rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_EXISTING_AND_LEAVE_OPEN));
660
661 for(ChangesetProcessingType type: ChangesetProcessingType.values()) {
662 rbChangesetHandlingOptions.get(type).setVisible(false);
663 bgChangesetHandlingOptions.add(rbChangesetHandlingOptions.get(type));
664 }
665 return pnl;
666 }
667
668 /**
669 * build the panel with the widgets for controlling how the changeset should be processed
670 * (atomic upload or not, comment, opening/closing changeset)
671 *
672 * @return
673 */
674 protected JPanel buildChangesetControlPanel() {
675 JPanel pnl = new JPanel();
676 pnl.setLayout(new BoxLayout(pnl, BoxLayout.Y_AXIS));
677 pnl.add(cbUseAtomicUpload = new JCheckBox(tr("upload all changes in one request")));
678 cbUseAtomicUpload.setToolTipText(tr("Enable to upload all changes in one request, disable to use one request per changed primitive"));
679 boolean useAtomicUpload = Main.pref.getBoolean("osm-server.atomic-upload", true);
680 cbUseAtomicUpload.setSelected(useAtomicUpload);
681 cbUseAtomicUpload.setEnabled(OsmApi.getOsmApi().hasSupportForDiffUploads());
682
683 pnl.add(buildChangesetHandlingControlPanel());
684 return pnl;
685 }
686
687 /**
688 * builds the upload control panel
689 *
690 * @return
691 */
692 protected JPanel buildUploadControlPanel() {
693 JPanel pnl = new JPanel();
694 pnl.setLayout(new GridBagLayout());
695 pnl.add(new JLabel(tr("Provide a brief comment for the changes you are uploading:")), GBC.eol().insets(0, 5, 10, 3));
696 cmt = new SuggestingJHistoryComboBox();
697 List<String> cmtHistory = new LinkedList<String>(Main.pref.getCollection(HISTORY_KEY, new LinkedList<String>()));
698 cmt.setHistory(cmtHistory);
699 pnl.add(cmt, GBC.eol().fill(GBC.HORIZONTAL));
700
701 // configuration options for atomic upload
702 //
703 pnl.add(buildChangesetControlPanel(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
704 return pnl;
705 }
706
707 /**
708 * builds the gui
709 */
710 protected void build() {
711 setLayout(new GridBagLayout());
712 GridBagConstraints gc = new GridBagConstraints();
713
714 // first the panel with the list in the upper half
715 //
716 gc.fill = GridBagConstraints.BOTH;
717 gc.weightx = 1.0;
718 gc.weighty = 1.0;
719 add(buildListsPanel(), gc);
720
721 // a tabbed pane with two configuration panels in the
722 // lower half
723 //
724 southTabbedPane = new JTabbedPane();
725 southTabbedPane.add(buildUploadControlPanel());
726 tagEditorPanel = new TagEditorPanel();
727 southTabbedPane.add(tagEditorPanel);
728 southTabbedPane.setTitleAt(0, tr("Settings"));
729 southTabbedPane.setTitleAt(1, tr("Tags of new changeset"));
730 JPanel pnl = new JPanel();
731 pnl.setLayout(new BorderLayout());
732 pnl.add(southTabbedPane,BorderLayout.CENTER);
733 gc.fill = GridBagConstraints.HORIZONTAL;
734 gc.gridy = 1;
735 gc.weightx = 1.0;
736 gc.weighty = 0.0;
737 add(pnl, gc);
738 }
739
740 /**
741 * constructor
742 */
743 protected UploadDialogPanel() {
744 OsmPrimitivRenderer renderer = new OsmPrimitivRenderer();
745
746 // initialize the three lists for primitives
747 //
748 lstAdd = new JList();
749 lstAdd.setCellRenderer(renderer);
750 lstAdd.setVisibleRowCount(Math.min(lstAdd.getModel().getSize(), 10));
751 spAdd = new JScrollPane(lstAdd);
752 lblAdd = new JLabel(tr("Objects to add:"));
753
754 lstUpdate = new JList();
755 lstUpdate.setCellRenderer(renderer);
756 lstUpdate.setVisibleRowCount(Math.min(lstUpdate.getModel().getSize(), 10));
757 spUpdate = new JScrollPane(lstUpdate);
758 lblUpdate = new JLabel(tr("Objects to modify:"));
759
760 lstDelete = new JList();
761 lstDelete.setCellRenderer(renderer);
762 lstDelete.setVisibleRowCount(Math.min(lstDelete.getModel().getSize(), 10));
763 spDelete = new JScrollPane(lstDelete);
764 lblDelete = new JLabel(tr("Objects to delete:"));
765
766 // build the GUI
767 //
768 build();
769 }
770
771 /**
772 * sets the collection of primitives which will be uploaded
773 *
774 * @param add the collection of primitives to add
775 * @param update the collection of primitives to update
776 * @param delete the collection of primitives to delete
777 */
778 public void setUploadedPrimitives(Collection<OsmPrimitive> add, Collection<OsmPrimitive> update, Collection<OsmPrimitive> delete) {
779 lstAdd.setListData(add.toArray());
780 lstUpdate.setListData(update.toArray());
781 lstDelete.setListData(delete.toArray());
782
783
784 GridBagConstraints gcLabel = new GridBagConstraints();
785 gcLabel.fill = GridBagConstraints.HORIZONTAL;
786 gcLabel.weightx = 1.0;
787 gcLabel.weighty = 0.0;
788 gcLabel.anchor = GridBagConstraints.FIRST_LINE_START;
789
790 GridBagConstraints gcList = new GridBagConstraints();
791 gcList.fill = GridBagConstraints.BOTH;
792 gcList.weightx = 1.0;
793 gcList.weighty = 1.0;
794 gcList.anchor = GridBagConstraints.CENTER;
795 pnlLists.removeAll();
796 int y = -1;
797 if (!add.isEmpty()) {
798 y++;
799 gcLabel.gridy = y;
800 lblAdd.setText(trn("{0} object to add:", "{0} objects to add:", add.size(),add.size()));
801 pnlLists.add(lblAdd, gcLabel);
802 y++;
803 gcList.gridy = y;
804 pnlLists.add(spAdd, gcList);
805 }
806 if (!update.isEmpty()) {
807 y++;
808 gcLabel.gridy = y;
809 lblUpdate.setText(trn("{0} object to modifiy:", "{0} objects to modify:", update.size(),update.size()));
810 pnlLists.add(lblUpdate, gcLabel);
811 y++;
812 gcList.gridy = y;
813 pnlLists.add(spUpdate, gcList);
814 }
815 if (!delete.isEmpty()) {
816 y++;
817 gcLabel.gridy = y;
818 lblDelete.setText(trn("{0} object to delete:", "{0} objects to delete:", delete.size(),delete.size()));
819 pnlLists.add(lblDelete, gcLabel);
820 y++;
821 gcList.gridy = y;
822 pnlLists.add(spDelete, gcList);
823 }
824 }
825
826 /**
827 * Replies true if a valid changeset comment has been entered in this dialog
828 *
829 * @return true if a valid changeset comment has been entered in this dialog
830 */
831 public boolean hasChangesetComment() {
832 if (!getChangesetProcessingType().isUseNew())
833 return true;
834 return cmt.getText().trim().length() >= 3;
835 }
836
837 /**
838 * Remembers the user input in the preference settings
839 */
840 public void rememberUserInput() {
841 // store the history of comments
842 cmt.addCurrentItemToHistory();
843 Main.pref.putCollection(HISTORY_KEY, cmt.getHistory());
844 Main.pref.put("osm-server.atomic-upload", cbUseAtomicUpload.isSelected());
845 }
846
847 /**
848 * Initializes the panel for user input
849 */
850 public void startUserInput() {
851 tagEditorPanel.initAutoCompletion(Main.main.getEditLayer());
852 initChangesetProcessingType();
853 cmt.getEditor().selectAll();
854 cmt.requestFocus();
855 }
856
857 /**
858 * Replies the current changeset processing type
859 *
860 * @return the current changeset processing type
861 */
862 public ChangesetProcessingType getChangesetProcessingType() {
863 ChangesetProcessingType changesetProcessingType = null;
864 for (ChangesetProcessingType type: ChangesetProcessingType.values()) {
865 if (rbChangesetHandlingOptions.get(type).isSelected()) {
866 changesetProcessingType = type;
867 break;
868 }
869 }
870 return changesetProcessingType == null ?
871 ChangesetProcessingType.USE_NEW_AND_CLOSE :
872 changesetProcessingType;
873 }
874
875 /**
876 * Replies the current changeset
877 *
878 * @return the current changeset
879 */
880 public Changeset getChangeset() {
881 Changeset changeset = new Changeset();
882 tagEditorPanel.getModel().applyToPrimitive(changeset);
883 changeset.put("comment", cmt.getText());
884 return changeset;
885 }
886
887 /**
888 * initializes the panel depending on the possible changeset processing
889 * types
890 */
891 protected void initChangesetProcessingType() {
892 for (ChangesetProcessingType type: ChangesetProcessingType.values()) {
893 // show options for new changeset, disable others
894 //
895 rbChangesetHandlingOptions.get(type).setVisible(type.isUseNew());
896 }
897 if (OsmApi.getOsmApi().getCurrentChangeset() != null) {
898 Changeset cs = OsmApi.getOsmApi().getCurrentChangeset();
899 for (ChangesetProcessingType type: ChangesetProcessingType.values()) {
900 // show options for using existing changeset
901 //
902 if (!type.isUseNew()) {
903 rbChangesetHandlingOptions.get(type).setVisible(true);
904 }
905 }
906 JRadioButton rb = rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_EXISTING_AND_CLOSE);
907 rb.setText(tr("Use the existing changeset {0} and close it after upload",cs.getId()));
908 rb.setToolTipText(tr("Select to upload to the existing changeset {0} and to close the changeset after this upload",cs.getId()));
909
910 rb = rbChangesetHandlingOptions.get(ChangesetProcessingType.USE_EXISTING_AND_LEAVE_OPEN);
911 rb.setText(tr("Use the existing changeset {0} and leave it open",cs.getId()));
912 rb.setToolTipText(tr("Select to upload to the existing changeset {0} and to leave the changeset open for further uploads",cs.getId()));
913
914 rbChangesetHandlingOptions.get(getChangesetProcessingType()).setSelected(true);
915
916 } else {
917 ChangesetProcessingType type = getChangesetProcessingType();
918 if (!type.isUseNew()) {
919 type = ChangesetProcessingType.USE_NEW_AND_CLOSE;
920 }
921 rbChangesetHandlingOptions.get(type).setSelected(true);
922 }
923 refreshChangesetProcessingType(getChangesetProcessingType());
924 }
925
926 /**
927 * refreshes the panel depending on a changeset processing type
928 *
929 * @param type the changeset processing type
930 */
931 protected void refreshChangesetProcessingType(ChangesetProcessingType type) {
932 if (type.isUseNew()) {
933 southTabbedPane.setTitleAt(1, tr("Tags of new changeset"));
934 Changeset cs = new Changeset();
935 tagEditorPanel.getModel().initFromPrimitive(cs);
936 } else {
937 Changeset cs = OsmApi.getOsmApi().getCurrentChangeset();
938 if (cs != null) {
939 southTabbedPane.setTitleAt(1, tr("Tags of changeset {0}", cs.getId()));
940 if (cs.get("comment") != null) {
941 cmt.setText(cs.get("comment"));
942 cs.remove("comment");
943 }
944 tagEditorPanel.getModel().initFromPrimitive(cs);
945 }
946 }
947 }
948
949 class ChangesetProcessingTypeChangedAction implements ActionListener {
950 public void actionPerformed(ActionEvent e) {
951 ChangesetProcessingType type = getChangesetProcessingType();
952 refreshChangesetProcessingType(type);
953 }
954 }
955 }
956}
Note: See TracBrowser for help on using the repository browser.