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 }