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;
011
012import org.openstreetmap.josm.command.Command;
013import org.openstreetmap.josm.command.conflict.CoordinateConflictResolveCommand;
014import org.openstreetmap.josm.command.conflict.DeletedStateConflictResolveCommand;
015import org.openstreetmap.josm.data.conflict.Conflict;
016import org.openstreetmap.josm.data.coor.LatLon;
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
020import org.openstreetmap.josm.gui.util.ChangeNotifier;
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 a {@link ChangeNotifier}. It notifies registered {@link javax.swing.event.ChangeListener}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 ChangeNotifier {
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        fireStateChanged();
148        fireCompletelyResolved();
149    }
150
151    /**
152     * replies the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
153     * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
154     *
155     * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
156     *  coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
157     */
158    public LatLon getMyCoords() {
159        return myCoords;
160    }
161
162    /**
163     * replies the coordinates of their {@link OsmPrimitive}. null, if their primitive hasn't
164     * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
165     *
166     * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
167     * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
168     */
169    public LatLon getTheirCoords() {
170        return theirCoords;
171    }
172
173    /**
174     * replies the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives
175     * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED}
176     *
177     * @return the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives
178     * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED}
179     */
180    public LatLon getMergedCoords() {
181        switch(coordMergeDecision) {
182        case KEEP_MINE: return myCoords;
183        case KEEP_THEIR: return theirCoords;
184        case UNDECIDED: return null;
185        }
186        // should not happen
187        return null;
188    }
189
190    /**
191     * Decides a conflict between local and server coordinates
192     *
193     * @param decision the decision
194     */
195    public void decideCoordsConflict(MergeDecisionType decision) {
196        coordMergeDecision = decision;
197        fireStateChanged();
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 if decision is null
263     */
264    public void decideDeletedStateConflict(MergeDecisionType decision) {
265        CheckParameterUtil.ensureParameterNotNull(decision, "decision");
266
267        boolean oldMergedDeletedState = getMergedDeletedState(this.deletedMergeDecision);
268        boolean newMergedDeletedState = getMergedDeletedState(decision);
269
270        this.deletedMergeDecision = decision;
271        fireStateChanged();
272        fireCompletelyResolved();
273
274        if (oldMergedDeletedState != newMergedDeletedState) {
275            support.firePropertyChange(DELETE_PRIMITIVE_PROP, oldMergedDeletedState, newMergedDeletedState);
276        }
277    }
278
279    /**
280     * replies true if my and their primitive have a conflict between
281     * their coordinate values
282     *
283     * @return true if my and their primitive have a conflict between
284     * their coordinate values; false otherwise
285     */
286    public boolean hasCoordConflict() {
287        if (myCoords == null && theirCoords != null) return true;
288        if (myCoords != null && theirCoords == null) return true;
289        if (myCoords == null && theirCoords == null) return false;
290        return myCoords != null && !myCoords.equalsEpsilon(theirCoords);
291    }
292
293    /**
294     * replies true if my and their primitive have a conflict between
295     * their deleted states
296     *
297     * @return <code>true</code> if my and their primitive have a conflict between
298     * their deleted states
299     */
300    public boolean hasDeletedStateConflict() {
301        return myDeletedState != theirDeletedState;
302    }
303
304    /**
305     * replies true if all conflict in this model are resolved
306     *
307     * @return <code>true</code> if all conflict in this model are resolved; <code>false</code> otherwise
308     */
309    public boolean isResolvedCompletely() {
310        boolean ret = true;
311        if (hasCoordConflict()) {
312            ret = ret && !coordMergeDecision.equals(UNDECIDED);
313        }
314        if (hasDeletedStateConflict()) {
315            ret = ret && !deletedMergeDecision.equals(UNDECIDED);
316        }
317        return ret;
318    }
319
320    /**
321     * Builds the command(s) to apply the conflict resolutions to my primitive
322     *
323     * @param conflict The conflict information
324     * @return The list of commands
325     */
326    public List<Command> buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) {
327        List<Command> cmds = new ArrayList<>();
328        if (hasCoordConflict() && isDecidedCoord()) {
329            cmds.add(new CoordinateConflictResolveCommand(conflict, coordMergeDecision));
330        }
331        if (hasDeletedStateConflict() && isDecidedDeletedState()) {
332            cmds.add(new DeletedStateConflictResolveCommand(conflict, deletedMergeDecision));
333        }
334        return cmds;
335    }
336
337    public OsmPrimitive getMyPrimitive() {
338        return my;
339    }
340
341}