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.JPanel; 015import javax.swing.JTabbedPane; 016 017import org.openstreetmap.josm.command.Command; 018import org.openstreetmap.josm.command.SequenceCommand; 019import org.openstreetmap.josm.command.conflict.ModifiedConflictResolveCommand; 020import org.openstreetmap.josm.command.conflict.VersionConflictResolveCommand; 021import org.openstreetmap.josm.data.conflict.Conflict; 022import org.openstreetmap.josm.data.osm.Node; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.Relation; 025import org.openstreetmap.josm.data.osm.Way; 026import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger; 027import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel; 028import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger; 029import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberMerger; 030import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeModel; 031import org.openstreetmap.josm.gui.conflict.pair.tags.TagMerger; 032import org.openstreetmap.josm.tools.ImageProvider; 033 034/** 035 * An UI component for resolving conflicts between two {@link OsmPrimitive}s. 036 * 037 * This component emits {@link PropertyChangeEvent}s for three properties: 038 * <ul> 039 * <li>{@link #RESOLVED_COMPLETELY_PROP} - new value is <code>true</code>, if the conflict is 040 * completely resolved</li> 041 * <li>{@link #MY_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of 042 * my primitive</li> 043 * <li>{@link #THEIR_PRIMITIVE_PROP} - new value is the {@link OsmPrimitive} in the role of 044 * their primitive</li> 045 * </ul> 046 * @since 1622 047 */ 048public class ConflictResolver extends JPanel implements PropertyChangeListener { 049 050 /* -------------------------------------------------------------------------------------- */ 051 /* Property names */ 052 /* -------------------------------------------------------------------------------------- */ 053 /** name of the property indicating whether all conflicts are resolved, 054 * {@link #isResolvedCompletely()} 055 */ 056 public static final String RESOLVED_COMPLETELY_PROP = ConflictResolver.class.getName() + ".resolvedCompletely"; 057 /** 058 * name of the property for the {@link OsmPrimitive} in the role "my" 059 */ 060 public static final String MY_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".myPrimitive"; 061 062 /** 063 * name of the property for the {@link OsmPrimitive} in the role "my" 064 */ 065 public static final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive"; 066 067 private JTabbedPane tabbedPane; 068 private TagMerger tagMerger; 069 private NodeListMerger nodeListMerger; 070 private RelationMemberMerger relationMemberMerger; 071 private PropertiesMerger propertiesMerger; 072 private final transient List<IConflictResolver> conflictResolvers = new ArrayList<>(); 073 private transient OsmPrimitive my; 074 private transient OsmPrimitive their; 075 private transient Conflict<? extends OsmPrimitive> conflict; 076 077 private ImageIcon mergeComplete; 078 private ImageIcon mergeIncomplete; 079 080 /** indicates whether the current conflict is resolved completely */ 081 private boolean resolvedCompletely; 082 083 /** 084 * loads the required icons 085 */ 086 protected final void loadIcons() { 087 mergeComplete = ImageProvider.get("dialogs", "valid"); 088 mergeIncomplete = ImageProvider.get("dialogs/conflict", "mergeincomplete"); 089 } 090 091 /** 092 * builds the UI 093 */ 094 protected final void build() { 095 tabbedPane = new JTabbedPane(); 096 097 propertiesMerger = new PropertiesMerger(); 098 propertiesMerger.setName("panel.propertiesmerger"); 099 propertiesMerger.getModel().addPropertyChangeListener(this); 100 tabbedPane.add(tr("Properties"), propertiesMerger); 101 102 tagMerger = new TagMerger(); 103 tagMerger.setName("panel.tagmerger"); 104 tagMerger.getModel().addPropertyChangeListener(this); 105 tabbedPane.add(tr("Tags"), tagMerger); 106 107 nodeListMerger = new NodeListMerger(); 108 nodeListMerger.setName("panel.nodelistmerger"); 109 nodeListMerger.getModel().addPropertyChangeListener(this); 110 tabbedPane.add(tr("Nodes"), nodeListMerger); 111 112 relationMemberMerger = new RelationMemberMerger(); 113 relationMemberMerger.setName("panel.relationmembermerger"); 114 relationMemberMerger.getModel().addPropertyChangeListener(this); 115 tabbedPane.add(tr("Members"), relationMemberMerger); 116 117 setLayout(new BorderLayout()); 118 add(tabbedPane, BorderLayout.CENTER); 119 120 conflictResolvers.add(propertiesMerger); 121 conflictResolvers.add(tagMerger); 122 conflictResolvers.add(nodeListMerger); 123 conflictResolvers.add(relationMemberMerger); 124 } 125 126 /** 127 * constructor 128 */ 129 public ConflictResolver() { 130 resolvedCompletely = false; 131 build(); 132 loadIcons(); 133 } 134 135 /** 136 * Sets the {@link OsmPrimitive} in the role "my" 137 * 138 * @param my the primitive in the role "my" 139 */ 140 protected void setMy(OsmPrimitive my) { 141 OsmPrimitive old = this.my; 142 this.my = my; 143 if (old != this.my) { 144 firePropertyChange(MY_PRIMITIVE_PROP, old, this.my); 145 } 146 } 147 148 /** 149 * Sets the {@link OsmPrimitive} in the role "their". 150 * 151 * @param their the primitive in the role "their" 152 */ 153 protected void setTheir(OsmPrimitive their) { 154 OsmPrimitive old = this.their; 155 this.their = their; 156 if (old != this.their) { 157 firePropertyChange(THEIR_PRIMITIVE_PROP, old, this.their); 158 } 159 } 160 161 /** 162 * handles property change events 163 * @param evt the event 164 * @see TagMergeModel 165 * @see AbstractListMergeModel 166 * @see PropertiesMergeModel 167 */ 168 @Override 169 public void propertyChange(PropertyChangeEvent evt) { 170 if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) { 171 int newValue = (Integer) evt.getNewValue(); 172 if (newValue == 0) { 173 tabbedPane.setTitleAt(1, tr("Tags")); 174 tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved")); 175 tabbedPane.setIconAt(1, mergeComplete); 176 } else { 177 tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue)); 178 tabbedPane.setToolTipTextAt(1, 179 trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue)); 180 tabbedPane.setIconAt(1, mergeIncomplete); 181 } 182 updateResolvedCompletely(); 183 } else if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) { 184 boolean frozen = (Boolean) evt.getNewValue(); 185 if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) { 186 if (frozen) { 187 tabbedPane.setTitleAt(2, tr("Nodes(resolved)")); 188 tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way")); 189 tabbedPane.setIconAt(2, mergeComplete); 190 } else { 191 tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)")); 192 tabbedPane.setToolTipTextAt(2, tr("Pending conflicts in the node list of this way")); 193 tabbedPane.setIconAt(2, mergeIncomplete); 194 } 195 } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) { 196 if (frozen) { 197 tabbedPane.setTitleAt(3, tr("Members(resolved)")); 198 tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation")); 199 tabbedPane.setIconAt(3, mergeComplete); 200 } else { 201 tabbedPane.setTitleAt(3, tr("Members(with conflicts)")); 202 tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation")); 203 tabbedPane.setIconAt(3, mergeIncomplete); 204 } 205 } 206 updateResolvedCompletely(); 207 } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) { 208 boolean resolved = (Boolean) evt.getNewValue(); 209 if (resolved) { 210 tabbedPane.setTitleAt(0, tr("Properties")); 211 tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts")); 212 tabbedPane.setIconAt(0, mergeComplete); 213 } else { 214 tabbedPane.setTitleAt(0, tr("Properties(with conflicts)")); 215 tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved")); 216 tabbedPane.setIconAt(0, mergeIncomplete); 217 } 218 updateResolvedCompletely(); 219 } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) { 220 for (IConflictResolver resolver: conflictResolvers) { 221 resolver.deletePrimitive((Boolean) evt.getNewValue()); 222 } 223 } 224 } 225 226 /** 227 * populates the conflict resolver with the conflicts between my and their 228 * 229 * @param conflict the conflict data set 230 */ 231 public void populate(Conflict<? extends OsmPrimitive> conflict) { 232 setMy(conflict.getMy()); 233 setTheir(conflict.getTheir()); 234 this.conflict = conflict; 235 propertiesMerger.populate(conflict); 236 237 tabbedPane.setEnabledAt(0, true); 238 tagMerger.populate(conflict); 239 tabbedPane.setEnabledAt(1, true); 240 241 if (my instanceof Node) { 242 tabbedPane.setEnabledAt(2, false); 243 tabbedPane.setEnabledAt(3, false); 244 } else if (my instanceof Way) { 245 nodeListMerger.populate(conflict); 246 tabbedPane.setEnabledAt(2, true); 247 tabbedPane.setEnabledAt(3, false); 248 tabbedPane.setTitleAt(3, tr("Members")); 249 tabbedPane.setIconAt(3, null); 250 } else if (my instanceof Relation) { 251 relationMemberMerger.populate(conflict); 252 tabbedPane.setEnabledAt(2, false); 253 tabbedPane.setTitleAt(2, tr("Nodes")); 254 tabbedPane.setIconAt(2, null); 255 tabbedPane.setEnabledAt(3, true); 256 } 257 updateResolvedCompletely(); 258 selectFirstTabWithConflicts(); 259 } 260 261 /** 262 * {@link JTabbedPane#setSelectedIndex(int) Selects} the first tab with conflicts 263 */ 264 public void selectFirstTabWithConflicts() { 265 for (int i = 0; i < tabbedPane.getTabCount(); i++) { 266 if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) { 267 tabbedPane.setSelectedIndex(i); 268 break; 269 } 270 } 271 } 272 273 /** 274 * Builds the resolution command(s) for the resolved conflicts in this ConflictResolver 275 * 276 * @return the resolution command 277 */ 278 public Command buildResolveCommand() { 279 List<Command> commands = new ArrayList<>(); 280 281 if (tagMerger.getModel().getNumResolvedConflicts() > 0) { 282 commands.add(tagMerger.getModel().buildResolveCommand(conflict)); 283 } 284 commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict)); 285 if (my instanceof Way && nodeListMerger.getModel().isFrozen()) { 286 commands.add(nodeListMerger.getModel().buildResolveCommand(conflict)); 287 } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) { 288 commands.add(relationMemberMerger.getModel().buildResolveCommand(conflict)); 289 } 290 if (isResolvedCompletely()) { 291 commands.add(new VersionConflictResolveCommand(conflict)); 292 commands.add(new ModifiedConflictResolveCommand(conflict)); 293 } 294 return new SequenceCommand(tr("Conflict Resolution"), commands); 295 } 296 297 /** 298 * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP} 299 * 300 */ 301 protected void updateResolvedCompletely() { 302 boolean oldValueResolvedCompletely = resolvedCompletely; 303 if (my instanceof Node) { 304 // resolve the version conflict if this is a node and all tag 305 // conflicts have been resolved 306 // 307 this.resolvedCompletely = 308 tagMerger.getModel().isResolvedCompletely() 309 && propertiesMerger.getModel().isResolvedCompletely(); 310 } else if (my instanceof Way) { 311 // resolve the version conflict if this is a way, all tag 312 // conflicts have been resolved, and conflicts in the node list 313 // have been resolved 314 // 315 this.resolvedCompletely = 316 tagMerger.getModel().isResolvedCompletely() 317 && propertiesMerger.getModel().isResolvedCompletely() 318 && nodeListMerger.getModel().isFrozen(); 319 } else if (my instanceof Relation) { 320 // resolve the version conflict if this is a relation, all tag 321 // conflicts and all conflicts in the member list 322 // have been resolved 323 // 324 this.resolvedCompletely = 325 tagMerger.getModel().isResolvedCompletely() 326 && propertiesMerger.getModel().isResolvedCompletely() 327 && relationMemberMerger.getModel().isFrozen(); 328 } 329 if (this.resolvedCompletely != oldValueResolvedCompletely) { 330 firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely); 331 } 332 } 333 334 /** 335 * Replies true all differences in this conflicts are resolved 336 * 337 * @return true all differences in this conflicts are resolved 338 */ 339 public boolean isResolvedCompletely() { 340 return resolvedCompletely; 341 } 342 343 /** 344 * Adds all registered listeners by this conflict resolver 345 * @see #unregisterListeners() 346 * @since 10454 347 */ 348 public void registerListeners() { 349 nodeListMerger.registerListeners(); 350 relationMemberMerger.registerListeners(); 351 } 352 353 /** 354 * Removes all registered listeners by this conflict resolver 355 */ 356 public void unregisterListeners() { 357 nodeListMerger.unregisterListeners(); 358 relationMemberMerger.unregisterListeners(); 359 } 360 361 /** 362 * {@link PropertiesMerger#decideRemaining(MergeDecisionType) Decides/resolves} undecided conflicts to the given decision type 363 * @param decision the decision to take for undecided conflicts 364 * @throws AssertionError if {@link #isResolvedCompletely()} does not hold after applying the decision 365 */ 366 public void decideRemaining(MergeDecisionType decision) { 367 propertiesMerger.decideRemaining(decision); 368 tagMerger.decideRemaining(decision); 369 if (my instanceof Way) { 370 nodeListMerger.decideRemaining(decision); 371 } else if (my instanceof Relation) { 372 relationMemberMerger.decideRemaining(decision); 373 } 374 updateResolvedCompletely(); 375 if (!isResolvedCompletely()) { 376 throw new AssertionError("The conflict could not be resolved completely!"); 377 } 378 } 379}