001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.gui.conflict.pair.properties; 003 004 import static org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType.UNDECIDED; 005 006 import java.beans.PropertyChangeListener; 007 import java.beans.PropertyChangeSupport; 008 import java.util.ArrayList; 009 import java.util.Collections; 010 import java.util.List; 011 import java.util.Observable; 012 013 import org.openstreetmap.josm.command.Command; 014 import org.openstreetmap.josm.command.CoordinateConflictResolveCommand; 015 import org.openstreetmap.josm.command.DeletedStateConflictResolveCommand; 016 import org.openstreetmap.josm.data.conflict.Conflict; 017 import org.openstreetmap.josm.data.coor.LatLon; 018 import org.openstreetmap.josm.data.osm.Node; 019 import org.openstreetmap.josm.data.osm.OsmPrimitive; 020 import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 021 import org.openstreetmap.josm.tools.CheckParameterUtil; 022 023 /** 024 * This is the model for resolving conflicts in the properties of the 025 * {@link OsmPrimitive}s. In particular, it represents conflicts in the coordinates of {@link Node}s and 026 * the deleted or visible state of {@link OsmPrimitive}s. 027 * 028 * This model is an {@link Observable}. It notifies registered {@link java.util.Observer}s whenever the 029 * internal state changes. 030 * 031 * This model also emits property changes for {@link #RESOLVED_COMPLETELY_PROP}. Property change 032 * listeners may register themselves using {@link #addPropertyChangeListener(PropertyChangeListener)}. 033 * 034 * @see Node#getCoor() 035 * @see OsmPrimitive#isDeleted 036 * @see OsmPrimitive#isVisible 037 * 038 */ 039 public class PropertiesMergeModel extends Observable { 040 041 static public final String RESOLVED_COMPLETELY_PROP = PropertiesMergeModel.class.getName() + ".resolvedCompletely"; 042 static public final String DELETE_PRIMITIVE_PROP = PropertiesMergeModel.class.getName() + ".deletePrimitive"; 043 044 private OsmPrimitive my; 045 046 private LatLon myCoords; 047 private LatLon theirCoords; 048 private MergeDecisionType coordMergeDecision; 049 050 private boolean myDeletedState; 051 private boolean theirDeletedState; 052 private List<OsmPrimitive> myReferrers; 053 private List<OsmPrimitive> theirReferrers; 054 private MergeDecisionType deletedMergeDecision; 055 private final PropertyChangeSupport support; 056 private Boolean resolvedCompletely; 057 058 public void addPropertyChangeListener(PropertyChangeListener listener) { 059 support.addPropertyChangeListener(listener); 060 } 061 062 public void removePropertyChangeListener(PropertyChangeListener listener) { 063 support.removePropertyChangeListener(listener); 064 } 065 066 public void fireCompletelyResolved() { 067 Boolean oldValue = resolvedCompletely; 068 resolvedCompletely = isResolvedCompletely(); 069 support.firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValue, resolvedCompletely); 070 } 071 072 public PropertiesMergeModel() { 073 coordMergeDecision = UNDECIDED; 074 deletedMergeDecision = UNDECIDED; 075 support = new PropertyChangeSupport(this); 076 resolvedCompletely = null; 077 } 078 079 /** 080 * replies true if there is a coordinate conflict and if this conflict is 081 * resolved 082 * 083 * @return true if there is a coordinate conflict and if this conflict is 084 * resolved; false, otherwise 085 */ 086 public boolean isDecidedCoord() { 087 return ! coordMergeDecision.equals(UNDECIDED); 088 } 089 090 /** 091 * replies true if there is a conflict in the deleted state and if this conflict is 092 * resolved 093 * 094 * @return true if there is a conflict in the deleted state and if this conflict is 095 * resolved; false, otherwise 096 */ 097 public boolean isDecidedDeletedState() { 098 return ! deletedMergeDecision.equals(UNDECIDED); 099 } 100 101 /** 102 * replies true if the current decision for the coordinate conflict is <code>decision</code> 103 * 104 * @return true if the current decision for the coordinate conflict is <code>decision</code>; 105 * false, otherwise 106 */ 107 public boolean isCoordMergeDecision(MergeDecisionType decision) { 108 return coordMergeDecision.equals(decision); 109 } 110 111 /** 112 * replies true if the current decision for the deleted state conflict is <code>decision</code> 113 * 114 * @return true if the current decision for the deleted state conflict is <code>decision</code>; 115 * false, otherwise 116 */ 117 public boolean isDeletedStateDecision(MergeDecisionType decision) { 118 return deletedMergeDecision.equals(decision); 119 } 120 121 /** 122 * Populates the model with the differences between local and server version 123 * 124 * @param conflict The conflict information 125 */ 126 public void populate(Conflict<? extends OsmPrimitive> conflict) { 127 this.my = conflict.getMy(); 128 OsmPrimitive their = conflict.getTheir(); 129 if (my instanceof Node) { 130 myCoords = ((Node)my).getCoor(); 131 theirCoords = ((Node)their).getCoor(); 132 } else { 133 myCoords = null; 134 theirCoords = null; 135 } 136 137 myDeletedState = conflict.isMyDeleted() || my.isDeleted(); 138 theirDeletedState = their.isDeleted(); 139 140 myReferrers = my.getDataSet() == null?Collections.<OsmPrimitive>emptyList():my.getReferrers(); 141 theirReferrers = their.getDataSet() == null?Collections.<OsmPrimitive>emptyList():their.getReferrers(); 142 143 coordMergeDecision = UNDECIDED; 144 deletedMergeDecision = UNDECIDED; 145 setChanged(); 146 notifyObservers(); 147 fireCompletelyResolved(); 148 } 149 150 /** 151 * replies the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 152 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 153 * 154 * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 155 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 156 */ 157 public LatLon getMyCoords() { 158 return myCoords; 159 } 160 161 /** 162 * replies the coordinates of their {@link OsmPrimitive}. null, if their primitive hasn't 163 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 164 * 165 * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 166 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 167 */ 168 public LatLon getTheirCoords() { 169 return theirCoords; 170 } 171 172 /** 173 * replies the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives 174 * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED} 175 * 176 * @return the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives 177 * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED} 178 */ 179 public LatLon getMergedCoords() { 180 switch(coordMergeDecision) { 181 case KEEP_MINE: return myCoords; 182 case KEEP_THEIR: return theirCoords; 183 case UNDECIDED: return null; 184 } 185 // should not happen 186 return null; 187 } 188 189 /** 190 * Decides a conflict between local and server coordinates 191 * 192 * @param decision the decision 193 */ 194 public void decideCoordsConflict(MergeDecisionType decision) { 195 coordMergeDecision = decision; 196 setChanged(); 197 notifyObservers(); 198 fireCompletelyResolved(); 199 } 200 201 /** 202 * Replies deleted state of local dataset 203 * @return The state of deleted flag 204 */ 205 public Boolean getMyDeletedState() { 206 return myDeletedState; 207 } 208 209 /** 210 * Replies deleted state of Server dataset 211 * @return The state of deleted flag 212 */ 213 public Boolean getTheirDeletedState() { 214 return theirDeletedState; 215 } 216 217 /** 218 * Replies deleted state of combined dataset 219 * @return The state of deleted flag 220 */ 221 public Boolean getMergedDeletedState() { 222 switch(deletedMergeDecision) { 223 case KEEP_MINE: return myDeletedState; 224 case KEEP_THEIR: return theirDeletedState; 225 case UNDECIDED: return null; 226 } 227 // should not happen 228 return null; 229 } 230 231 /** 232 * Returns local referrers 233 * @return The referrers 234 */ 235 public List<OsmPrimitive> getMyReferrers() { 236 return myReferrers; 237 } 238 239 /** 240 * Returns server referrers 241 * @return The referrers 242 */ 243 public List<OsmPrimitive> getTheirReferrers() { 244 return theirReferrers; 245 } 246 247 private boolean getMergedDeletedState(MergeDecisionType decision) { 248 switch (decision) { 249 case KEEP_MINE: 250 return myDeletedState; 251 case KEEP_THEIR: 252 return theirDeletedState; 253 default: 254 return false; 255 } 256 } 257 258 /** 259 * decides the conflict between two deleted states 260 * @param decision the decision (must not be null) 261 * 262 * @throws IllegalArgumentException thrown, if decision is null 263 */ 264 public void decideDeletedStateConflict(MergeDecisionType decision) throws IllegalArgumentException{ 265 CheckParameterUtil.ensureParameterNotNull(decision, "decision"); 266 267 boolean oldMergedDeletedState = getMergedDeletedState(this.deletedMergeDecision); 268 boolean newMergedDeletedState = getMergedDeletedState(decision); 269 270 this.deletedMergeDecision = decision; 271 setChanged(); 272 notifyObservers(); 273 fireCompletelyResolved(); 274 275 if (oldMergedDeletedState != newMergedDeletedState) { 276 support.firePropertyChange(DELETE_PRIMITIVE_PROP, oldMergedDeletedState, newMergedDeletedState); 277 } 278 } 279 280 /** 281 * replies true if my and their primitive have a conflict between 282 * their coordinate values 283 * 284 * @return true if my and their primitive have a conflict between 285 * their coordinate values; false otherwise 286 */ 287 public boolean hasCoordConflict() { 288 if (myCoords == null && theirCoords != null) return true; 289 if (myCoords != null && theirCoords == null) return true; 290 if (myCoords == null && theirCoords == null) return false; 291 return !myCoords.equalsEpsilon(theirCoords); 292 } 293 294 /** 295 * replies true if my and their primitive have a conflict between 296 * their deleted states 297 * 298 * @return <code>true</code> if my and their primitive have a conflict between 299 * their deleted states 300 */ 301 public boolean hasDeletedStateConflict() { 302 return myDeletedState != theirDeletedState; 303 } 304 305 /** 306 * replies true if all conflict in this model are resolved 307 * 308 * @return <code>true</code> if all conflict in this model are resolved; <code>false</code> otherwise 309 */ 310 public boolean isResolvedCompletely() { 311 boolean ret = true; 312 if (hasCoordConflict()) { 313 ret = ret && ! coordMergeDecision.equals(UNDECIDED); 314 } 315 if (hasDeletedStateConflict()) { 316 ret = ret && ! deletedMergeDecision.equals(UNDECIDED); 317 } 318 return ret; 319 } 320 321 /** 322 * Builds the command(s) to apply the conflict resolutions to my primitive 323 * 324 * @param conflict The conflict information 325 * @return The list of commands 326 */ 327 public List<Command> buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) { 328 List<Command> cmds = new ArrayList<Command>(); 329 if (hasCoordConflict() && isDecidedCoord()) { 330 cmds.add(new CoordinateConflictResolveCommand(conflict, coordMergeDecision)); 331 } 332 if (hasDeletedStateConflict() && isDecidedDeletedState()) { 333 cmds.add(new DeletedStateConflictResolveCommand(conflict, deletedMergeDecision)); 334 } 335 return cmds; 336 } 337 338 public OsmPrimitive getMyPrimitive() { 339 return my; 340 } 341 342 }