001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.pair; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.BorderLayout; 008import java.beans.PropertyChangeEvent; 009import java.beans.PropertyChangeListener; 010import java.util.ArrayList; 011import java.util.List; 012 013import javax.swing.ImageIcon; 014import javax.swing.JComponent; 015import javax.swing.JPanel; 016import javax.swing.JScrollPane; 017import javax.swing.JTabbedPane; 018 019import org.openstreetmap.josm.command.Command; 020import org.openstreetmap.josm.command.SequenceCommand; 021import org.openstreetmap.josm.command.conflict.ModifiedConflictResolveCommand; 022import org.openstreetmap.josm.command.conflict.VersionConflictResolveCommand; 023import org.openstreetmap.josm.data.conflict.Conflict; 024import org.openstreetmap.josm.data.osm.Node; 025import org.openstreetmap.josm.data.osm.OsmPrimitive; 026import org.openstreetmap.josm.data.osm.Relation; 027import org.openstreetmap.josm.data.osm.Way; 028import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger; 029import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel; 030import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger; 031import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberMerger; 032import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeModel; 033import org.openstreetmap.josm.gui.conflict.pair.tags.TagMerger; 034import org.openstreetmap.josm.tools.ImageProvider; 035 036/** 037 * An UI component for resolving conflicts between two {@link OsmPrimitive}s. 038 * 039 * This component emits {@link PropertyChangeEvent}s for three properties: 040 * <ul> 041 * <li>{@link #RESOLVED_COMPLETELY_PROP} - new value is <code>true</code>, if the conflict is 042 * completely resolved</li> 043 * <li>{@link #MY_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of 044 * my primitive</li> 045 * <li>{@link #THEIR_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of 046 * their primitive</li> 047 * </ul> 048 * @since 1622 049 */ 050public class ConflictResolver extends JPanel implements PropertyChangeListener { 051 052 /* -------------------------------------------------------------------------------------- */ 053 /* Property names */ 054 /* -------------------------------------------------------------------------------------- */ 055 /** name of the property indicating whether all conflicts are resolved, 056 * {@link #isResolvedCompletely()} 057 */ 058 public static final String RESOLVED_COMPLETELY_PROP = ConflictResolver.class.getName() + ".resolvedCompletely"; 059 /** 060 * name of the property for the {@link OsmPrimitive} in the role "my" 061 */ 062 public static final String MY_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".myPrimitive"; 063 064 /** 065 * name of the property for the {@link OsmPrimitive} in the role "my" 066 */ 067 public static final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive"; 068 069 private JTabbedPane tabbedPane; 070 private TagMerger tagMerger; 071 private NodeListMerger nodeListMerger; 072 private RelationMemberMerger relationMemberMerger; 073 private PropertiesMerger propertiesMerger; 074 private final transient List<IConflictResolver> conflictResolvers = new ArrayList<>(); 075 private transient OsmPrimitive my; 076 private transient OsmPrimitive their; 077 private transient Conflict<? extends OsmPrimitive> conflict; 078 079 private ImageIcon mergeComplete; 080 private ImageIcon mergeIncomplete; 081 082 /** indicates whether the current conflict is resolved completely */ 083 private boolean resolvedCompletely; 084 085 /** 086 * loads the required icons 087 */ 088 protected final void loadIcons() { 089 mergeComplete = ImageProvider.get("misc", "green_check"); 090 mergeIncomplete = ImageProvider.get("dialogs/conflict", "mergeincomplete"); 091 } 092 093 /** 094 * builds the UI 095 */ 096 protected final void build() { 097 tabbedPane = new JTabbedPane(); 098 099 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}