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

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

fixed #3422: Upload dialog allows modification of the created_by=* tag when adding to an existing changeset

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