source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java@ 5266

Last change on this file since 5266 was 5266, checked in by bastiK, 12 years ago

fixed majority of javadoc warnings by replacing "{@see" by "{@link"

  • Property svn:eol-style set to native
File size: 21.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.tags;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.BorderLayout;
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.FlowLayout;
12import java.awt.event.ActionEvent;
13import java.awt.event.HierarchyBoundsListener;
14import java.awt.event.HierarchyEvent;
15import java.awt.event.WindowAdapter;
16import java.awt.event.WindowEvent;
17import java.beans.PropertyChangeEvent;
18import java.beans.PropertyChangeListener;
19import java.util.Collection;
20import java.util.HashSet;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Set;
24
25import javax.swing.AbstractAction;
26import javax.swing.Action;
27import javax.swing.JDialog;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JSplitPane;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.actions.ExpertToggleAction;
35import org.openstreetmap.josm.command.ChangePropertyCommand;
36import org.openstreetmap.josm.command.Command;
37import org.openstreetmap.josm.corrector.UserCancelException;
38import org.openstreetmap.josm.data.osm.NameFormatter;
39import org.openstreetmap.josm.data.osm.Node;
40import org.openstreetmap.josm.data.osm.OsmPrimitive;
41import org.openstreetmap.josm.data.osm.Relation;
42import org.openstreetmap.josm.data.osm.TagCollection;
43import org.openstreetmap.josm.data.osm.Way;
44import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
45import org.openstreetmap.josm.gui.DefaultNameFormatter;
46import org.openstreetmap.josm.gui.SideButton;
47import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
48import org.openstreetmap.josm.gui.help.HelpUtil;
49import org.openstreetmap.josm.tools.ImageProvider;
50import org.openstreetmap.josm.tools.Utils;
51import org.openstreetmap.josm.tools.Utils.Function;
52import org.openstreetmap.josm.tools.WindowGeometry;
53
54/**
55 * This dialog helps to resolve conflicts occurring when ways are combined or
56 * nodes are merged.
57 *
58 * Usage: {@link #launchIfNecessary} followed by {@link #buildResolutionCommands}.
59 *
60 * Prior to {@link #launchIfNecessary}, the following usage sequence was needed:
61 *
62 * There is a singleton instance of this dialog which can be retrieved using
63 * {@link #getInstance()}.
64 *
65 * The dialog uses two models: one for resolving tag conflicts, the other
66 * for resolving conflicts in relation memberships. For both models there are accessors,
67 * i.e {@link #getTagConflictResolverModel()} and {@link #getRelationMemberConflictResolverModel()}.
68 *
69 * Models have to be <strong>populated</strong> before the dialog is launched. Example:
70 * <pre>
71 * CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
72 * dialog.getTagConflictResolverModel().populate(aTagCollection);
73 * dialog.getRelationMemberConflictResolverModel().populate(aRelationLinkCollection);
74 * dialog.prepareDefaultDecisions();
75 * </pre>
76 *
77 * You should also set the target primitive which other primitives (ways or nodes) are
78 * merged to, see {@link #setTargetPrimitive(OsmPrimitive)}.
79 *
80 * After the dialog is closed use {@link #isCanceled()} to check whether the user canceled
81 * the dialog. If it wasn't canceled you may build a collection of {@link Command} objects
82 * which reflect the conflict resolution decisions the user made in the dialog:
83 * see {@link #buildResolutionCommands()}
84 *
85 *
86 */
87public class CombinePrimitiveResolverDialog extends JDialog {
88
89 /** the unique instance of the dialog */
90 static private CombinePrimitiveResolverDialog instance;
91
92 /**
93 * Replies the unique instance of the dialog
94 *
95 * @return the unique instance of the dialog
96 * @deprecated use {@link #launchIfNecessary} instead.
97 */
98 @Deprecated
99 public static CombinePrimitiveResolverDialog getInstance() {
100 if (instance == null) {
101 instance = new CombinePrimitiveResolverDialog(Main.parent);
102 }
103 return instance;
104 }
105
106 private AutoAdjustingSplitPane spTagConflictTypes;
107 private TagConflictResolver pnlTagConflictResolver;
108 private RelationMemberConflictResolver pnlRelationMemberConflictResolver;
109 private boolean canceled;
110 private JPanel pnlButtons;
111 private OsmPrimitive targetPrimitive;
112
113 /** the private help action */
114 private ContextSensitiveHelpAction helpAction;
115 /** the apply button */
116 private SideButton btnApply;
117
118 /**
119 * Replies the target primitive the collection of primitives is merged
120 * or combined to.
121 *
122 * @return the target primitive
123 */
124 public OsmPrimitive getTargetPrimitmive() {
125 return targetPrimitive;
126 }
127
128 /**
129 * Sets the primitive the collection of primitives is merged or combined
130 * to.
131 *
132 * @param primitive the target primitive
133 */
134 public void setTargetPrimitive(OsmPrimitive primitive) {
135 this.targetPrimitive = primitive;
136 updateTitle();
137 if (primitive instanceof Way) {
138 pnlRelationMemberConflictResolver.initForWayCombining();
139 } else if (primitive instanceof Node) {
140 pnlRelationMemberConflictResolver.initForNodeMerging();
141 }
142 }
143
144 protected void updateTitle() {
145 if (targetPrimitive == null) {
146 setTitle(tr("Conflicts when combining primitives"));
147 return;
148 }
149 if (targetPrimitive instanceof Way) {
150 setTitle(tr("Conflicts when combining ways - combined way is ''{0}''", targetPrimitive
151 .getDisplayName(DefaultNameFormatter.getInstance())));
152 helpAction.setHelpTopic(ht("/Action/CombineWay#ResolvingConflicts"));
153 getRootPane().putClientProperty("help", ht("/Action/CombineWay#ResolvingConflicts"));
154 } else if (targetPrimitive instanceof Node) {
155 setTitle(tr("Conflicts when merging nodes - target node is ''{0}''", targetPrimitive
156 .getDisplayName(DefaultNameFormatter.getInstance())));
157 helpAction.setHelpTopic(ht("/Action/MergeNodes#ResolvingConflicts"));
158 getRootPane().putClientProperty("help", ht("/Action/MergeNodes#ResolvingConflicts"));
159 }
160 }
161
162 protected void build() {
163 getContentPane().setLayout(new BorderLayout());
164 updateTitle();
165 spTagConflictTypes = new AutoAdjustingSplitPane(JSplitPane.VERTICAL_SPLIT);
166 spTagConflictTypes.setTopComponent(buildTagConflictResolverPanel());
167 spTagConflictTypes.setBottomComponent(buildRelationMemberConflictResolverPanel());
168 getContentPane().add(pnlButtons = buildButtonPanel(), BorderLayout.SOUTH);
169 addWindowListener(new AdjustDividerLocationAction());
170 HelpUtil.setHelpContext(getRootPane(), ht("/"));
171 }
172
173 protected JPanel buildTagConflictResolverPanel() {
174 pnlTagConflictResolver = new TagConflictResolver();
175 return pnlTagConflictResolver;
176 }
177
178 protected JPanel buildRelationMemberConflictResolverPanel() {
179 pnlRelationMemberConflictResolver = new RelationMemberConflictResolver();
180 return pnlRelationMemberConflictResolver;
181 }
182
183 protected JPanel buildButtonPanel() {
184 JPanel pnl = new JPanel();
185 pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
186
187 // -- apply button
188 ApplyAction applyAction = new ApplyAction();
189 pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction);
190 pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction);
191 btnApply = new SideButton(applyAction);
192 btnApply.setFocusable(true);
193 pnl.add(btnApply);
194
195 // -- cancel button
196 CancelAction cancelAction = new CancelAction();
197 pnl.add(new SideButton(cancelAction));
198
199 // -- help button
200 helpAction = new ContextSensitiveHelpAction();
201 pnl.add(new SideButton(helpAction));
202
203 return pnl;
204 }
205
206 public CombinePrimitiveResolverDialog(Component owner) {
207 super(JOptionPane.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL);
208 build();
209 }
210
211 public TagConflictResolverModel getTagConflictResolverModel() {
212 return pnlTagConflictResolver.getModel();
213 }
214
215 public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() {
216 return pnlRelationMemberConflictResolver.getModel();
217 }
218
219 protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) {
220 LinkedList<Command> cmds = new LinkedList<Command>();
221 for (String key : tc.getKeys()) {
222 if (tc.hasUniqueEmptyValue(key)) {
223 if (primitive.get(key) != null) {
224 cmds.add(new ChangePropertyCommand(primitive, key, null));
225 }
226 } else {
227 String value = tc.getJoinedValues(key);
228 if (!value.equals(primitive.get(key))) {
229 cmds.add(new ChangePropertyCommand(primitive, key, value));
230 }
231 }
232 }
233 return cmds;
234 }
235
236 public List<Command> buildResolutionCommands() {
237 List<Command> cmds = new LinkedList<Command>();
238
239 TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions();
240 if (allResolutions.size() > 0) {
241 cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions));
242 }
243 if (targetPrimitive.get("created_by") != null) {
244 cmds.add(new ChangePropertyCommand(targetPrimitive, "created_by", null));
245 }
246
247 if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) {
248 cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive));
249 }
250
251 Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel()
252 .getModifiedRelations(targetPrimitive));
253 if (cmd != null) {
254 cmds.add(cmd);
255 }
256 return cmds;
257 }
258
259 protected void prepareDefaultTagDecisions() {
260 TagConflictResolverModel model = getTagConflictResolverModel();
261 for (int i = 0; i < model.getRowCount(); i++) {
262 MultiValueResolutionDecision decision = model.getDecision(i);
263 List<String> values = decision.getValues();
264 values.remove("");
265 if (values.size() == 1) {
266 decision.keepOne(values.get(0));
267 } else {
268 decision.keepAll();
269 }
270 }
271 model.rebuild();
272 }
273
274 protected void prepareDefaultRelationDecisions() {
275 RelationMemberConflictResolverModel model = getRelationMemberConflictResolverModel();
276 Set<Relation> relations = new HashSet<Relation>();
277 for (int i = 0; i < model.getNumDecisions(); i++) {
278 RelationMemberConflictDecision decision = model.getDecision(i);
279 if (!relations.contains(decision.getRelation())) {
280 decision.decide(RelationMemberConflictDecisionType.KEEP);
281 relations.add(decision.getRelation());
282 } else {
283 decision.decide(RelationMemberConflictDecisionType.REMOVE);
284 }
285 }
286 model.refresh();
287 }
288
289 public void prepareDefaultDecisions() {
290 prepareDefaultTagDecisions();
291 prepareDefaultRelationDecisions();
292 }
293
294 protected JPanel buildEmptyConflictsPanel() {
295 JPanel pnl = new JPanel();
296 pnl.setLayout(new BorderLayout());
297 pnl.add(new JLabel(tr("No conflicts to resolve")));
298 return pnl;
299 }
300
301 protected void prepareGUIBeforeConflictResolutionStarts() {
302 RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel();
303 TagConflictResolverModel tagModel = getTagConflictResolverModel();
304 getContentPane().removeAll();
305
306 if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) {
307 // display both, the dialog for resolving relation conflicts and for resolving
308 // tag conflicts
309 spTagConflictTypes.setTopComponent(pnlTagConflictResolver);
310 spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver);
311 getContentPane().add(spTagConflictTypes, BorderLayout.CENTER);
312 } else if (relModel.getNumDecisions() > 0) {
313 // relation conflicts only
314 //
315 getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER);
316 } else if (tagModel.getNumDecisions() > 0) {
317 // tag conflicts only
318 //
319 getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER);
320 } else {
321 getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER);
322 }
323
324 getContentPane().add(pnlButtons, BorderLayout.SOUTH);
325 validate();
326 int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
327 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
328 if (numTagDecisions > 0 && numRelationDecisions > 0) {
329 spTagConflictTypes.setDividerLocation(0.5);
330 }
331 pnlRelationMemberConflictResolver.prepareForEditing();
332 }
333
334 protected void setCanceled(boolean canceled) {
335 this.canceled = canceled;
336 }
337
338 public boolean isCanceled() {
339 return canceled;
340 }
341
342 @Override
343 public void setVisible(boolean visible) {
344 if (visible) {
345 prepareGUIBeforeConflictResolutionStarts();
346 new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent,
347 new Dimension(600, 400))).applySafe(this);
348 setCanceled(false);
349 btnApply.requestFocusInWindow();
350 } else {
351 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
352 }
353 super.setVisible(visible);
354 }
355
356 class CancelAction extends AbstractAction {
357
358 public CancelAction() {
359 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
360 putValue(Action.NAME, tr("Cancel"));
361 putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
362 setEnabled(true);
363 }
364
365 public void actionPerformed(ActionEvent arg0) {
366 setCanceled(true);
367 setVisible(false);
368 }
369 }
370
371 class ApplyAction extends AbstractAction implements PropertyChangeListener {
372
373 public ApplyAction() {
374 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
375 putValue(Action.NAME, tr("Apply"));
376 putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
377 updateEnabledState();
378 }
379
380 public void actionPerformed(ActionEvent arg0) {
381 setVisible(false);
382 pnlTagConflictResolver.rememberPreferences();
383 }
384
385 protected void updateEnabledState() {
386 setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0
387 && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0);
388 }
389
390 public void propertyChange(PropertyChangeEvent evt) {
391 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
392 updateEnabledState();
393 }
394 if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) {
395 updateEnabledState();
396 }
397 }
398 }
399
400 class AdjustDividerLocationAction extends WindowAdapter {
401 @Override
402 public void windowOpened(WindowEvent e) {
403 int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
404 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
405 if (numTagDecisions > 0 && numRelationDecisions > 0) {
406 spTagConflictTypes.setDividerLocation(0.5);
407 }
408 }
409 }
410
411 static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener {
412 private double dividerLocation;
413
414 public AutoAdjustingSplitPane(int newOrientation) {
415 super(newOrientation);
416 addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
417 addHierarchyBoundsListener(this);
418 }
419
420 public void ancestorResized(HierarchyEvent e) {
421 setDividerLocation((int) (dividerLocation * getHeight()));
422 }
423
424 public void ancestorMoved(HierarchyEvent e) {
425 // do nothing
426 }
427
428 public void propertyChange(PropertyChangeEvent evt) {
429 if (evt.getPropertyName().equals(JSplitPane.DIVIDER_LOCATION_PROPERTY)) {
430 int newVal = (Integer) evt.getNewValue();
431 if (getHeight() != 0) {
432 dividerLocation = (double) newVal / (double) getHeight();
433 }
434 }
435 }
436 }
437
438 public static List<Command> launchIfNecessary(
439 final TagCollection tagsOfPrimitives,
440 final Collection<? extends OsmPrimitive> primitives,
441 final Collection<? extends OsmPrimitive> targetPrimitives) throws UserCancelException {
442
443 final TagCollection completeWayTags = new TagCollection(tagsOfPrimitives);
444 TagConflictResolutionUtil.combineTigerTags(completeWayTags);
445 TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(completeWayTags, primitives);
446 final TagCollection tagsToEdit = new TagCollection(completeWayTags);
447 TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit);
448
449 final CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
450
451 dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues());
452
453 final Set<Relation> parentRelations = OsmPrimitive.getParentRelations(primitives);
454 dialog.getRelationMemberConflictResolverModel().populate(parentRelations, primitives);
455 dialog.prepareDefaultDecisions();
456
457 // show information dialog to non-experts
458 if (!completeWayTags.isApplicableToPrimitive() && !ExpertToggleAction.isExpert()) {
459 String conflicts = Utils.joinAsHtmlUnorderedList(Utils.transform(completeWayTags.getKeysWithMultipleValues(), new Function<String, String>() {
460
461 @Override
462 public String apply(String key) {
463 return tr("{0} ({1})", key, Utils.join(tr(", "), Utils.transform(completeWayTags.getValues(key), new Function<String, String>() {
464
465 @Override
466 public String apply(String x) {
467 return x == null || x.isEmpty() ? tr("<i>missing</i>") : x;
468 }
469 })));
470 }
471 }));
472 String msg = tr("You are about to combine {0} objects, "
473 + "but the following tags are used conflictingly:<br/>{1}"
474 + "If these objects are combined, the resulting object may have unwanted tags.<br/>"
475 + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>"
476 + "Do you want to continue?",
477 primitives.size(), conflicts);
478 if (!ConditionalOptionPaneUtil.showConfirmationDialog(
479 "combine_tags",
480 Main.parent,
481 "<html>" + msg + "</html>",
482 tr("Combine confirmation"),
483 JOptionPane.YES_NO_OPTION,
484 JOptionPane.QUESTION_MESSAGE,
485 JOptionPane.YES_OPTION)) {
486 throw new UserCancelException();
487 }
488 }
489
490 if (!parentRelations.isEmpty() && !ExpertToggleAction.isExpert()) {
491 String msg = trn("You are about to combine {1} objects, "
492 + "which are part of {0} relation:<br/>{2}"
493 + "Combining these objects may break this relation. If you are unsure, please cancel this operation.<br/>"
494 + "If you want to continue, you are shown a dialog to decide how to adapt the relation.<br/><br/>"
495 + "Do you want to continue?",
496 "You are about to combine {1} objects, "
497 + "which are part of {0} relations:<br/>{2}"
498 + "Combining these objects may break these relations. If you are unsure, please cancel this operation.<br/>"
499 + "If you want to continue, you are shown a dialog to decide how to adapt the relations.<br/><br/>"
500 + "Do you want to continue?",
501 parentRelations.size(), parentRelations.size(), primitives.size(),
502 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(parentRelations));
503 if (!ConditionalOptionPaneUtil.showConfirmationDialog(
504 "combine_tags",
505 Main.parent,
506 "<html>" + msg + "</html>",
507 tr("Combine confirmation"),
508 JOptionPane.YES_NO_OPTION,
509 JOptionPane.QUESTION_MESSAGE,
510 JOptionPane.YES_OPTION)) {
511 throw new UserCancelException();
512 }
513 }
514
515 // resolve tag conflicts if necessary
516 if (!completeWayTags.isApplicableToPrimitive() || !parentRelations.isEmpty()) {
517 dialog.setVisible(true);
518 if (dialog.isCanceled()) {
519 throw new UserCancelException();
520 }
521 }
522 List<Command> cmds = new LinkedList<Command>();
523 for (OsmPrimitive i : targetPrimitives) {
524 dialog.setTargetPrimitive(i);
525 cmds.addAll(dialog.buildResolutionCommands());
526 }
527 return cmds;
528 }
529}
Note: See TracBrowser for help on using the repository browser.