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