001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.gui.conflict.pair; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 import static org.openstreetmap.josm.tools.I18n.trn; 006 007 import java.awt.BorderLayout; 008 import java.beans.PropertyChangeEvent; 009 import java.beans.PropertyChangeListener; 010 import java.util.ArrayList; 011 import java.util.List; 012 013 import javax.swing.ImageIcon; 014 import javax.swing.JPanel; 015 import javax.swing.JTabbedPane; 016 017 import org.openstreetmap.josm.command.Command; 018 import org.openstreetmap.josm.command.ModifiedConflictResolveCommand; 019 import org.openstreetmap.josm.command.SequenceCommand; 020 import org.openstreetmap.josm.command.VersionConflictResolveCommand; 021 import org.openstreetmap.josm.data.conflict.Conflict; 022 import org.openstreetmap.josm.data.osm.Node; 023 import org.openstreetmap.josm.data.osm.OsmPrimitive; 024 import org.openstreetmap.josm.data.osm.Relation; 025 import org.openstreetmap.josm.data.osm.Way; 026 import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMergeModel; 027 import org.openstreetmap.josm.gui.conflict.pair.nodes.NodeListMerger; 028 import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMergeModel; 029 import org.openstreetmap.josm.gui.conflict.pair.properties.PropertiesMerger; 030 import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberListMergeModel; 031 import org.openstreetmap.josm.gui.conflict.pair.relation.RelationMemberMerger; 032 import org.openstreetmap.josm.gui.conflict.pair.tags.TagMergeModel; 033 import org.openstreetmap.josm.gui.conflict.pair.tags.TagMerger; 034 import 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 * 049 */ 050 public 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 static public 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 static public 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 static public final String THEIR_PRIMITIVE_PROP = ConflictResolver.class.getName() + ".theirPrimitive"; 068 069 private JTabbedPane tabbedPane = null; 070 private TagMerger tagMerger; 071 private NodeListMerger nodeListMerger; 072 private RelationMemberMerger relationMemberMerger; 073 private PropertiesMerger propertiesMerger; 074 private final List<IConflictResolver> conflictResolvers = new ArrayList<IConflictResolver>(); 075 private OsmPrimitive my; 076 private OsmPrimitive their; 077 private 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 void loadIcons() { 089 mergeComplete = ImageProvider.get("dialogs/conflict","mergecomplete.png" ); 090 mergeIncomplete = ImageProvider.get("dialogs/conflict","mergeincomplete.png" ); 091 } 092 093 /** 094 * builds the UI 095 */ 096 protected void build() { 097 tabbedPane = new JTabbedPane(); 098 099 propertiesMerger = new PropertiesMerger(); 100 propertiesMerger.setName("panel.propertiesmerger"); 101 propertiesMerger.getModel().addPropertyChangeListener(this); 102 tabbedPane.add(tr("Properties"), propertiesMerger); 103 104 tagMerger = new TagMerger(); 105 tagMerger.setName("panel.tagmerger"); 106 tagMerger.getModel().addPropertyChangeListener(this); 107 tabbedPane.add(tr("Tags"), tagMerger); 108 109 nodeListMerger = new NodeListMerger(); 110 nodeListMerger.setName("panel.nodelistmerger"); 111 nodeListMerger.getModel().addPropertyChangeListener(this); 112 tabbedPane.add(tr("Nodes"), nodeListMerger); 113 114 relationMemberMerger = new RelationMemberMerger(); 115 relationMemberMerger.setName("panel.relationmembermerger"); 116 relationMemberMerger.getModel().addPropertyChangeListener(this); 117 tabbedPane.add(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 /** 129 * constructor 130 */ 131 public ConflictResolver() { 132 resolvedCompletely = false; 133 build(); 134 loadIcons(); 135 } 136 137 /** 138 * Sets the {@link OsmPrimitive} in the role "my" 139 * 140 * @param my the primitive in the role "my" 141 */ 142 protected void setMy(OsmPrimitive my) { 143 OsmPrimitive old = this.my; 144 this.my = my; 145 if (old != this.my) { 146 firePropertyChange(MY_PRIMITIVE_PROP, old, this.my); 147 } 148 } 149 150 /** 151 * Sets the {@link OsmPrimitive} in the role "their". 152 * 153 * @param their the primitive in the role "their" 154 */ 155 protected void setTheir(OsmPrimitive their) { 156 OsmPrimitive old = this.their; 157 this.their = their; 158 if (old != this.their) { 159 firePropertyChange(THEIR_PRIMITIVE_PROP, old, this.their); 160 } 161 } 162 163 /** 164 * handles property change events 165 */ 166 public void propertyChange(PropertyChangeEvent evt) { 167 if (evt.getPropertyName().equals(TagMergeModel.PROP_NUM_UNDECIDED_TAGS)) { 168 int newValue = (Integer)evt.getNewValue(); 169 if (newValue == 0) { 170 tabbedPane.setTitleAt(1, tr("Tags")); 171 tabbedPane.setToolTipTextAt(1, tr("No pending tag conflicts to be resolved")); 172 tabbedPane.setIconAt(1, mergeComplete); 173 } else { 174 tabbedPane.setTitleAt(1, trn("Tags({0} conflict)", "Tags({0} conflicts)", newValue, newValue)); 175 tabbedPane.setToolTipTextAt(1, trn("{0} pending tag conflict to be resolved", "{0} pending tag conflicts to be resolved", newValue, newValue)); 176 tabbedPane.setIconAt(1, mergeIncomplete); 177 } 178 updateResolvedCompletely(); 179 } else if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) { 180 boolean frozen = (Boolean)evt.getNewValue(); 181 if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) { 182 if (frozen) { 183 tabbedPane.setTitleAt(2, tr("Nodes(resolved)")); 184 tabbedPane.setToolTipTextAt(2, tr("Merged node list frozen. No pending conflicts in the node list of this way")); 185 tabbedPane.setIconAt(2, mergeComplete); 186 } else { 187 tabbedPane.setTitleAt(2, tr("Nodes(with conflicts)")); 188 tabbedPane.setToolTipTextAt(2,tr("Pending conflicts in the node list of this way")); 189 tabbedPane.setIconAt(2, mergeIncomplete); 190 } 191 } else if (evt.getSource() == relationMemberMerger.getModel() && my instanceof Relation) { 192 if (frozen) { 193 tabbedPane.setTitleAt(3, tr("Members(resolved)")); 194 tabbedPane.setToolTipTextAt(3, tr("Merged member list frozen. No pending conflicts in the member list of this relation")); 195 tabbedPane.setIconAt(3, mergeComplete); 196 } else { 197 tabbedPane.setTitleAt(3, tr("Members(with conflicts)")); 198 tabbedPane.setToolTipTextAt(3, tr("Pending conflicts in the member list of this relation")); 199 tabbedPane.setIconAt(3, mergeIncomplete); 200 } 201 } 202 updateResolvedCompletely(); 203 } else if (evt.getPropertyName().equals(PropertiesMergeModel.RESOLVED_COMPLETELY_PROP)) { 204 boolean resolved = (Boolean)evt.getNewValue(); 205 if (resolved) { 206 tabbedPane.setTitleAt(0, tr("Properties")); 207 tabbedPane.setToolTipTextAt(0, tr("No pending property conflicts")); 208 tabbedPane.setIconAt(0, mergeComplete); 209 } else { 210 tabbedPane.setTitleAt(0, tr("Properties(with conflicts)")); 211 tabbedPane.setToolTipTextAt(0, tr("Pending property conflicts to be resolved")); 212 tabbedPane.setIconAt(0, mergeIncomplete); 213 } 214 updateResolvedCompletely(); 215 } else if (PropertiesMergeModel.DELETE_PRIMITIVE_PROP.equals(evt.getPropertyName())) { 216 for (IConflictResolver resolver: conflictResolvers) { 217 resolver.deletePrimitive((Boolean) evt.getNewValue()); 218 } 219 } 220 } 221 222 /** 223 * populates the conflict resolver with the conflicts between my and their 224 * 225 * @param my my primitive (i.e. the primitive in the local dataset) 226 * @param their their primitive (i.e. the primitive in the server dataset) 227 * 228 */ 229 public void populate(Conflict<? extends OsmPrimitive> conflict) { 230 setMy(conflict.getMy()); 231 setTheir(conflict.getTheir()); 232 this.conflict = conflict; 233 propertiesMerger.populate(conflict); 234 235 tabbedPane.setEnabledAt(0, true); 236 tagMerger.populate(conflict); 237 tabbedPane.setEnabledAt(1, true); 238 239 if (my instanceof Node) { 240 tabbedPane.setEnabledAt(2,false); 241 tabbedPane.setEnabledAt(3,false); 242 } else if (my instanceof Way) { 243 nodeListMerger.populate(conflict); 244 tabbedPane.setEnabledAt(2, true); 245 tabbedPane.setEnabledAt(3, false); 246 tabbedPane.setTitleAt(3,tr("Members")); 247 tabbedPane.setIconAt(3, null); 248 } else if (my instanceof Relation) { 249 relationMemberMerger.populate(conflict); 250 tabbedPane.setEnabledAt(2, false); 251 tabbedPane.setTitleAt(2,tr("Nodes")); 252 tabbedPane.setIconAt(2, null); 253 tabbedPane.setEnabledAt(3, true); 254 } 255 updateResolvedCompletely(); 256 selectFirstTabWithConflicts(); 257 } 258 259 public void selectFirstTabWithConflicts() { 260 for (int i = 0; i < tabbedPane.getTabCount(); i++) { 261 if (tabbedPane.isEnabledAt(i) && mergeIncomplete.equals(tabbedPane.getIconAt(i))) { 262 tabbedPane.setSelectedIndex(i); 263 break; 264 } 265 } 266 } 267 268 /** 269 * Builds the resolution command(s) for the resolved conflicts in this 270 * ConflictResolver 271 * 272 * @return the resolution command 273 */ 274 public Command buildResolveCommand() { 275 ArrayList<Command> commands = new ArrayList<Command>(); 276 277 if (tagMerger.getModel().getNumResolvedConflicts() > 0) { 278 commands.add(tagMerger.getModel().buildResolveCommand(conflict)); 279 } 280 commands.addAll(propertiesMerger.getModel().buildResolveCommand(conflict)); 281 if (my instanceof Way && nodeListMerger.getModel().isFrozen()) { 282 NodeListMergeModel model = (NodeListMergeModel) nodeListMerger.getModel(); 283 commands.add(model.buildResolveCommand(conflict)); 284 } else if (my instanceof Relation && relationMemberMerger.getModel().isFrozen()) { 285 RelationMemberListMergeModel model = (RelationMemberListMergeModel) relationMemberMerger.getModel(); 286 commands.add(model.buildResolveCommand((Relation) my, (Relation) their)); 287 } 288 if (isResolvedCompletely()) { 289 commands.add(new VersionConflictResolveCommand(conflict)); 290 commands.add(new ModifiedConflictResolveCommand(conflict)); 291 } 292 return new SequenceCommand(tr("Conflict Resolution"), commands); 293 } 294 295 /** 296 * Updates the state of the property {@link #RESOLVED_COMPLETELY_PROP} 297 * 298 */ 299 protected void updateResolvedCompletely() { 300 boolean oldValueResolvedCompletely = resolvedCompletely; 301 if (my instanceof Node) { 302 // resolve the version conflict if this is a node and all tag 303 // conflicts have been resolved 304 // 305 this.resolvedCompletely = 306 tagMerger.getModel().isResolvedCompletely() 307 && propertiesMerger.getModel().isResolvedCompletely(); 308 } else if (my instanceof Way) { 309 // resolve the version conflict if this is a way, all tag 310 // conflicts have been resolved, and conflicts in the node list 311 // have been resolved 312 // 313 this.resolvedCompletely = 314 tagMerger.getModel().isResolvedCompletely() 315 && propertiesMerger.getModel().isResolvedCompletely() 316 && nodeListMerger.getModel().isFrozen(); 317 } else if (my instanceof Relation) { 318 // resolve the version conflict if this is a relation, all tag 319 // conflicts and all conflicts in the member list 320 // have been resolved 321 // 322 this.resolvedCompletely = 323 tagMerger.getModel().isResolvedCompletely() 324 && propertiesMerger.getModel().isResolvedCompletely() 325 && relationMemberMerger.getModel().isFrozen(); 326 } 327 if (this.resolvedCompletely != oldValueResolvedCompletely) { 328 firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValueResolvedCompletely, this.resolvedCompletely); 329 } 330 } 331 332 /** 333 * Replies true all differences in this conflicts are resolved 334 * 335 * @return true all differences in this conflicts are resolved 336 */ 337 public boolean isResolvedCompletely() { 338 return resolvedCompletely; 339 } 340 341 public void unregisterListeners() { 342 nodeListMerger.unlinkAsListener(); 343 relationMemberMerger.unlinkAsListener(); 344 } 345 }