source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/pair/ConflictResolver.java@ 17340

Last change on this file since 17340 was 17340, checked in by GerdP, 3 years ago

fix #19792: Error during conflict resolution after complex import operation

  • ConflictResolver: initialize resolvedCompletely in populate()
  • fix typo in PropertiesMerger
  • create copies of lists in buildResolveCommand() implementations
  • revert r17273 (not needed)
  • Property svn:eol-style set to native
File size: 15.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.pair;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.beans.PropertyChangeEvent;
9import java.beans.PropertyChangeListener;
10import java.util.ArrayList;
11import java.util.List;
12
13import javax.swing.ImageIcon;
14import javax.swing.JComponent;
15import javax.swing.JPanel;
16import javax.swing.JScrollPane;
17import javax.swing.JTabbedPane;
18
19import org.openstreetmap.josm.command.Command;
20import org.openstreetmap.josm.command.SequenceCommand;
21import org.openstreetmap.josm.command.conflict.ModifiedConflictResolveCommand;
22import org.openstreetmap.josm.command.conflict.VersionConflictResolveCommand;
23import org.openstreetmap.josm.data.conflict.Conflict;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger;
29import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel;
30import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger;
31import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberMerger;
32import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeModel;
33import org.openstreetmap.josm.gui.conflict.pair.tags.TagMerger;
34import org.openstreetmap.josm.tools.ImageProvider;
35
36/**
37 * An UI component for resolving conflicts between two {@link OsmPrimitive}s.
38 *
39 * This component emits {@link PropertyChangeEvent}s for three properties:
40 * <ul>
41 * <li>{@link #RESOLVED_COMPLETELY_PROP} - new value is <code>true</code>, if the conflict is
42 * completely resolved</li>
43 * <li>{@link #MY_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of
44 * my primitive</li>
45 * <li>{@link #THEIR_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of
46 * their primitive</li>
47 * </ul>
48 * @since 1622
49 */
50public class ConflictResolver extends JPanel implements PropertyChangeListener {
51
52 /* -------------------------------------------------------------------------------------- */
53 /* Property names */
54 /* -------------------------------------------------------------------------------------- */
55 /** name of the property indicating whether all conflicts are resolved,
56 * {@link #isResolvedCompletely()}
57 */
58 public static final String RESOLVED_COMPLETELY_PROP = ConflictResolver.class.getName() + ".resolvedCompletely";
59 /**
60 * name of the property for the {@link OsmPrimitive} in the role "my"
61 */
62 public static final String MY_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".myPrimitive";
63
64 /**
65 * name of the property for the {@link OsmPrimitive} in the role "my"
66 */
67 public static final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive";
68
69 private JTabbedPane tabbedPane;
70 private TagMerger tagMerger;
71 private NodeListMerger nodeListMerger;
72 private RelationMemberMerger relationMemberMerger;
73 private PropertiesMerger propertiesMerger;
74 private final transient List<IConflictResolver> conflictResolvers = new ArrayList<>();
75 private transient OsmPrimitive my;
76 private transient OsmPrimitive their;
77 private transient Conflict<? extends OsmPrimitive> conflict;
78
79 private ImageIcon mergeComplete;
80 private ImageIcon mergeIncomplete;
81
82 /** indicates whether the current conflict is resolved completely */
83 private boolean resolvedCompletely;
84
85 /**
86 * loads the required icons
87 */
88 protected final void loadIcons() {
89 mergeComplete = ImageProvider.get("misc", "green_check");
90 mergeIncomplete = ImageProvider.get("dialogs/conflict", "mergeincomplete");
91 }
92
93 /**
94 * builds the UI
95 */
96 protected final void build() {
97 tabbedPane = new JTabbedPane();
98
99 propertiesMerger = new PropertiesMerger();
100 propertiesMerger.setName("panel.propertiesmerger");
101 propertiesMerger.getModel().addPropertyChangeListener(this);
102 addTab(tr("Properties"), propertiesMerger);
103
104 tagMerger = new TagMerger();
105 tagMerger.setName("panel.tagmerger");
106 tagMerger.getModel().addPropertyChangeListener(this);
107 addTab(tr("Tags"), tagMerger);
108
109 nodeListMerger = new NodeListMerger();
110 nodeListMerger.setName("panel.nodelistmerger");
111 nodeListMerger.getModel().addPropertyChangeListener(this);
112 addTab(tr("Nodes"), nodeListMerger);
113
114 relationMemberMerger = new RelationMemberMerger();
115 relationMemberMerger.setName("panel.relationmembermerger");
116 relationMemberMerger.getModel().addPropertyChangeListener(this);
117 addTab(tr("Members"), relationMemberMerger);
118
119 setLayout(new BorderLayout());
120 add(tabbedPane, BorderLayout.CENTER);
121
122 conflictResolvers.add(propertiesMerger);
123 conflictResolvers.add(tagMerger);
124 conflictResolvers.add(nodeListMerger);
125 conflictResolvers.add(relationMemberMerger);
126 }
127
128 private void addTab(String title, JComponent tabContent) {
129 JScrollPane scrollPanel = new JScrollPane(tabContent);
130 tabbedPane.add(title, scrollPanel);
131 }
132
133 /**
134 * constructor
135 */
136 public ConflictResolver() {
137 build();
138 loadIcons();
139 }
140
141 /**
142 * Sets the {@link OsmPrimitive} in the role "my"
143 *
144 * @param my the primitive in the role "my"
145 */
146 protected void setMy(OsmPrimitive my) {
147 OsmPrimitive old = this.my;
148 this.my = my;
149 if (old != this.my) {
150 firePropertyChange(MY_PRIMITIVE_PROP, old, this.my);
151 }
152 }
153
154 /**
155 * Sets the {@link OsmPrimitive} in the role "their".
156 *
157 * @param their the primitive in the role "their"
158 */
159 protected void setTheir(OsmPrimitive their) {
160 OsmPrimitive old = this.their;
161 this.their = their;
162 if (old != this.their) {
163 firePropertyChange(THEIR_PRIMITIVE_PROP, old, this.their);
164 }
165 }
166
167 /**
168 * handles property change events
169 * @param evt the event
170 * @see TagMergeModel
171 * @see AbstractListMergeModel
172 * @see PropertiesMergeModel
173 */
174 @Override
175 public void propertyChange(PropertyChangeEvent evt) {
176 if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) {
177 int newValue = (Integer) evt.getNewValue();
178 if (newValue == 0) {
179 tabbedPane.setTitleAt(1, tr("Tags"));
180 tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved"));
181 tabbedPane.setIconAt(1, mergeComplete);
182 } else {
183 tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue));
184 tabbedPane.setToolTipTextAt(1,
185 trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue));
186 tabbedPane.setIconAt(1, mergeIncomplete);
187 }
188 updateResolvedCompletely();
189 } else if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) {
190 boolean frozen = (Boolean) evt.getNewValue();
191 if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) {
192 if (frozen) {
193 tabbedPane.setTitleAt(2, tr("Nodes(resolved)"));
194 tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way"));
195 tabbedPane.setIconAt(2, mergeComplete);
196 } else {
197 tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)"));
198 tabbedPane.setToolTipTextAt(2, tr("Pending conflicts in the node list of this way"));
199 tabbedPane.setIconAt(2, mergeIncomplete);
200 }
201 } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) {
202 if (frozen) {
203 tabbedPane.setTitleAt(3, tr("Members(resolved)"));
204 tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation"));
205 tabbedPane.setIconAt(3, mergeComplete);
206 } else {
207 tabbedPane.setTitleAt(3, tr("Members(with conflicts)"));
208 tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation"));
209 tabbedPane.setIconAt(3, mergeIncomplete);
210 }
211 }
212 updateResolvedCompletely();
213 } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) {
214 boolean resolved = (Boolean) evt.getNewValue();
215 if (resolved) {
216 tabbedPane.setTitleAt(0, tr("Properties"));
217 tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts"));
218 tabbedPane.setIconAt(0, mergeComplete);
219 } else {
220 tabbedPane.setTitleAt(0, tr("Properties(with conflicts)"));
221 tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved"));
222 tabbedPane.setIconAt(0, mergeIncomplete);
223 }
224 updateResolvedCompletely();
225 } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) {
226 for (IConflictResolver resolver: conflictResolvers) {
227 resolver.deletePrimitive((Boolean) evt.getNewValue());
228 }
229 }
230 }
231
232 /**
233 * populates the conflict resolver with the conflicts between my and their
234 *
235 * @param conflict the conflict data set
236 */
237 public void populate(Conflict<? extends OsmPrimitive> conflict) {
238 setMy(conflict.getMy());
239 setTheir(conflict.getTheir());
240 this.conflict = conflict;
241 this.resolvedCompletely = false;
242 propertiesMerger.populate(conflict);
243
244 tabbedPane.setEnabledAt(0, true);
245 tagMerger.populate(conflict);
246 tabbedPane.setEnabledAt(1, true);
247
248 if (my instanceof Node) {
249 tabbedPane.setEnabledAt(2, false);
250 tabbedPane.setEnabledAt(3, false);
251 } else if (my instanceof Way) {
252 nodeListMerger.populate(conflict);
253 tabbedPane.setEnabledAt(2, true);
254 tabbedPane.setEnabledAt(3, false);
255 tabbedPane.setTitleAt(3, tr("Members"));
256 tabbedPane.setIconAt(3, null);
257 } else if (my instanceof Relation) {
258 relationMemberMerger.populate(conflict);
259 tabbedPane.setEnabledAt(2, false);
260 tabbedPane.setTitleAt(2, tr("Nodes"));
261 tabbedPane.setIconAt(2, null);
262 tabbedPane.setEnabledAt(3, true);
263 }
264 updateResolvedCompletely();
265 selectFirstTabWithConflicts();
266 }
267
268 /**
269 * {@link JTabbedPane#setSelectedIndex(int) Selects} the first tab with conflicts
270 */
271 public void selectFirstTabWithConflicts() {
272 for (int i = 0; i < tabbedPane.getTabCount(); i++) {
273 if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) {
274 tabbedPane.setSelectedIndex(i);
275 break;
276 }
277 }
278 }
279
280 /**
281 * Builds the resolution command(s) for the resolved conflicts in this ConflictResolver
282 *
283 * @return the resolution command
284 */
285 public Command buildResolveCommand() {
286 List<Command> commands = new ArrayList<>();
287
288 if (tagMerger.getModel().getNumResolvedConflicts() > 0) {
289 commands.add(tagMerger.getModel().buildResolveCommand(conflict));
290 }
291 commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict));
292 if (my instanceof Way && nodeListMerger.getModel().isFrozen()) {
293 commands.add(nodeListMerger.getModel().buildResolveCommand(conflict));
294 } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) {
295 commands.add(relationMemberMerger.getModel().buildResolveCommand(conflict));
296 }
297 if (isResolvedCompletely()) {
298 commands.add(new VersionConflictResolveCommand(conflict));
299 commands.add(new ModifiedConflictResolveCommand(conflict));
300 }
301 return new SequenceCommand(tr("Conflict Resolution"), commands);
302 }
303
304 /**
305 * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP}
306 *
307 */
308 protected void updateResolvedCompletely() {
309 boolean oldValueResolvedCompletely = resolvedCompletely;
310 if (my instanceof Node) {
311 // resolve the version conflict if this is a node and all tag
312 // conflicts have been resolved
313 //
314 this.resolvedCompletely =
315 tagMerger.getModel().isResolvedCompletely()
316 && propertiesMerger.getModel().isResolvedCompletely();
317 } else if (my instanceof Way) {
318 // resolve the version conflict if this is a way, all tag
319 // conflicts have been resolved, and conflicts in the node list
320 // have been resolved
321 //
322 this.resolvedCompletely =
323 tagMerger.getModel().isResolvedCompletely()
324 && propertiesMerger.getModel().isResolvedCompletely()
325 && nodeListMerger.getModel().isFrozen();
326 } else if (my instanceof Relation) {
327 // resolve the version conflict if this is a relation, all tag
328 // conflicts and all conflicts in the member list
329 // have been resolved
330 //
331 this.resolvedCompletely =
332 tagMerger.getModel().isResolvedCompletely()
333 && propertiesMerger.getModel().isResolvedCompletely()
334 && relationMemberMerger.getModel().isFrozen();
335 }
336 if (this.resolvedCompletely != oldValueResolvedCompletely) {
337 firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely);
338 }
339 }
340
341 /**
342 * Replies true all differences in this conflicts are resolved
343 *
344 * @return true all differences in this conflicts are resolved
345 */
346 public boolean isResolvedCompletely() {
347 return resolvedCompletely;
348 }
349
350 /**
351 * Adds all registered listeners by this conflict resolver
352 * @see #unregisterListeners()
353 * @since 10454
354 */
355 public void registerListeners() {
356 nodeListMerger.registerListeners();
357 relationMemberMerger.registerListeners();
358 }
359
360 /**
361 * Removes all registered listeners by this conflict resolver
362 */
363 public void unregisterListeners() {
364 nodeListMerger.unregisterListeners();
365 relationMemberMerger.unregisterListeners();
366 }
367
368 /**
369 * {@link PropertiesMerger#decideRemaining(MergeDecisionType) Decides/resolves} undecided conflicts to the given decision type
370 * @param decision the decision to take for undecided conflicts
371 * @throws AssertionError if {@link #isResolvedCompletely()} does not hold after applying the decision
372 */
373 public void decideRemaining(MergeDecisionType decision) {
374 propertiesMerger.decideRemaining(decision);
375 tagMerger.decideRemaining(decision);
376 if (my instanceof Way) {
377 nodeListMerger.decideRemaining(decision);
378 } else if (my instanceof Relation) {
379 relationMemberMerger.decideRemaining(decision);
380 }
381 updateResolvedCompletely();
382 if (!isResolvedCompletely()) {
383 throw new AssertionError("The conflict could not be resolved completely!");
384 }
385 }
386}
Note: See TracBrowser for help on using the repository browser.