001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.tags;
003
004import java.beans.PropertyChangeListener;
005import java.beans.PropertyChangeSupport;
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.HashSet;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Set;
012
013import javax.swing.table.DefaultTableModel;
014
015import org.openstreetmap.josm.command.ChangeCommand;
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.data.osm.OsmPrimitive;
018import org.openstreetmap.josm.data.osm.Relation;
019import org.openstreetmap.josm.data.osm.RelationMember;
020import org.openstreetmap.josm.data.osm.RelationToChildReference;
021import org.openstreetmap.josm.gui.util.GuiHelper;
022
023/**
024 * This model manages a list of conflicting relation members.
025 *
026 * It can be used as {@link javax.swing.table.TableModel}.
027 */
028public class RelationMemberConflictResolverModel extends DefaultTableModel {
029    /** the property name for the number conflicts managed by this model */
030    public static final String NUM_CONFLICTS_PROP = RelationMemberConflictResolverModel.class.getName() + ".numConflicts";
031
032    /** the list of conflict decisions */
033    protected final List<RelationMemberConflictDecision> decisions;
034    /** the collection of relations for which we manage conflicts */
035    protected Collection<Relation> relations;
036    /** the number of conflicts */
037    private int numConflicts;
038    private final PropertyChangeSupport support;
039
040    /**
041     * Replies true if each {@link MultiValueResolutionDecision} is decided.
042     *
043     * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise
044     */
045    public boolean isResolvedCompletely() {
046        return numConflicts == 0;
047    }
048
049    /**
050     * Replies the current number of conflicts
051     *
052     * @return the current number of conflicts
053     */
054    public int getNumConflicts() {
055        return numConflicts;
056    }
057
058    /**
059     * Updates the current number of conflicts from list of decisions and emits
060     * a property change event if necessary.
061     *
062     */
063    protected void updateNumConflicts() {
064        int count = 0;
065        for (RelationMemberConflictDecision decision: decisions) {
066            if (!decision.isDecided()) {
067                count++;
068            }
069        }
070        int oldValue = numConflicts;
071        numConflicts = count;
072        if (numConflicts != oldValue) {
073            support.firePropertyChange(getProperty(), oldValue, numConflicts);
074        }
075    }
076
077    protected String getProperty() {
078        return NUM_CONFLICTS_PROP;
079    }
080
081    public void addPropertyChangeListener(PropertyChangeListener l) {
082        support.addPropertyChangeListener(l);
083    }
084
085    public void removePropertyChangeListener(PropertyChangeListener l) {
086        support.removePropertyChangeListener(l);
087    }
088
089    public RelationMemberConflictResolverModel() {
090        decisions = new ArrayList<>();
091        support = new PropertyChangeSupport(this);
092    }
093
094    @Override
095    public int getRowCount() {
096        return getNumDecisions();
097    }
098
099    @Override
100    public Object getValueAt(int row, int column) {
101        if (decisions == null) return null;
102
103        RelationMemberConflictDecision d = decisions.get(row);
104        switch(column) {
105        case 0: /* relation */ return d.getRelation();
106        case 1: /* pos */ return Integer.toString(d.getPos() + 1); // position in "user space" starting at 1
107        case 2: /* role */ return d.getRole();
108        case 3: /* original */ return d.getOriginalPrimitive();
109        case 4: /* decision */ return d.getDecision();
110        }
111        return null;
112    }
113
114    @Override
115    public void setValueAt(Object value, int row, int column) {
116        RelationMemberConflictDecision d = decisions.get(row);
117        switch(column) {
118        case 2: /* role */
119            d.setRole((String)value);
120            break;
121        case 4: /* decision */
122            d.decide((RelationMemberConflictDecisionType)value);
123            refresh();
124            break;
125        }
126        fireTableDataChanged();
127    }
128
129    /**
130     * Populates the model with the members of the relation <code>relation</code>
131     * referring to <code>primitive</code>.
132     *
133     * @param relation the parent relation
134     * @param primitive the child primitive
135     */
136    protected void populate(Relation relation, OsmPrimitive primitive) {
137        for (int i =0; i<relation.getMembersCount();i++) {
138            if (relation.getMember(i).refersTo(primitive)) {
139                decisions.add(new RelationMemberConflictDecision(relation, i));
140            }
141        }
142    }
143
144    /**
145     * Populates the model with the relation members belonging to one of the relations in <code>relations</code>
146     * and referring to one of the primitives in <code>memberPrimitives</code>.
147     *
148     * @param relations  the parent relations. Empty list assumed if null.
149     * @param memberPrimitives the child primitives. Empty list assumed if null.
150     */
151    public void populate(Collection<Relation> relations, Collection<? extends OsmPrimitive> memberPrimitives) {
152        decisions.clear();
153        relations = relations == null ? new LinkedList<Relation>() : relations;
154        memberPrimitives = memberPrimitives == null ? new LinkedList<OsmPrimitive>() : memberPrimitives;
155        for (Relation r : relations) {
156            for (OsmPrimitive p: memberPrimitives) {
157                populate(r,p);
158            }
159        }
160        this.relations = relations;
161        refresh();
162    }
163
164    /**
165     * Populates the model with the relation members represented as a collection of
166     * {@link RelationToChildReference}s.
167     *
168     * @param references the references. Empty list assumed if null.
169     */
170    public void populate(Collection<RelationToChildReference> references) {
171        references = references == null ? new LinkedList<RelationToChildReference>() : references;
172        decisions.clear();
173        this.relations = new HashSet<>(references.size());
174        for (RelationToChildReference reference: references) {
175            decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition()));
176            relations.add(reference.getParent());
177        }
178        refresh();
179    }
180
181    /**
182     * Replies the decision at position <code>row</code>
183     *
184     * @param row
185     * @return the decision at position <code>row</code>
186     */
187    public RelationMemberConflictDecision getDecision(int row) {
188        return decisions.get(row);
189    }
190
191    /**
192     * Replies the number of decisions managed by this model
193     *
194     * @return the number of decisions managed by this model
195     */
196    public int getNumDecisions() {
197        return decisions == null ? 0 : decisions.size();
198    }
199
200    /**
201     * Refreshes the model state. Invoke this method to trigger necessary change
202     * events after an update of the model data.
203     *
204     */
205    public void refresh() {
206        updateNumConflicts();
207        GuiHelper.runInEDTAndWait(new Runnable() {
208            @Override public void run() {
209                fireTableDataChanged();
210            }
211        });
212    }
213
214    /**
215     * Apply a role to all member managed by this model.
216     *
217     * @param role the role. Empty string assumed if null.
218     */
219    public void applyRole(String role) {
220        role = role == null ? "" : role;
221        for (RelationMemberConflictDecision decision : decisions) {
222            decision.setRole(role);
223        }
224        refresh();
225    }
226
227    protected RelationMemberConflictDecision getDecision(Relation relation, int pos) {
228        for(RelationMemberConflictDecision decision: decisions) {
229            if (decision.matches(relation, pos)) return decision;
230        }
231        return null;
232    }
233
234    protected Command buildResolveCommand(Relation relation, OsmPrimitive newPrimitive) {
235        final Relation modifiedRelation = new Relation(relation);
236        modifiedRelation.setMembers(null);
237        boolean isChanged = false;
238        for (int i=0; i < relation.getMembersCount(); i++) {
239            final RelationMember member = relation.getMember(i);
240            RelationMemberConflictDecision decision = getDecision(relation, i);
241            if (decision == null) {
242                modifiedRelation.addMember(member);
243            } else {
244                switch(decision.getDecision()) {
245                case KEEP:
246                    final RelationMember newMember = new RelationMember(decision.getRole(),newPrimitive);
247                    modifiedRelation.addMember(newMember);
248                    isChanged |= ! member.equals(newMember);
249                    break;
250                case REMOVE:
251                    isChanged = true;
252                    // do nothing
253                    break;
254                case UNDECIDED:
255                    // FIXME: this is an error
256                    break;
257                }
258            }
259        }
260        if (isChanged)
261            return new ChangeCommand(relation, modifiedRelation);
262        return null;
263    }
264
265    /**
266     * Builds a collection of commands executing the decisions made in this model.
267     *
268     * @param newPrimitive the primitive which members shall refer to
269     * @return a list of commands
270     */
271    public List<Command> buildResolutionCommands(OsmPrimitive newPrimitive) {
272        List<Command> command = new LinkedList<>();
273        for (Relation relation : relations) {
274            Command cmd = buildResolveCommand(relation, newPrimitive);
275            if (cmd != null) {
276                command.add(cmd);
277            }
278        }
279        return command;
280    }
281
282    protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) {
283        for (int i=0; i < relation.getMembersCount(); i++) {
284            RelationMemberConflictDecision decision = getDecision(relation, i);
285            if (decision == null) {
286                continue;
287            }
288            switch(decision.getDecision()) {
289            case REMOVE: return true;
290            case KEEP:
291                if (!relation.getMember(i).getRole().equals(decision.getRole()))
292                    return true;
293                if (relation.getMember(i).getMember() != newPrimitive)
294                    return true;
295            case UNDECIDED:
296                // FIXME: handle error
297            }
298        }
299        return false;
300    }
301
302    /**
303     * Replies the set of relations which have to be modified according
304     * to the decisions managed by this model.
305     *
306     * @param newPrimitive the primitive which members shall refer to
307     *
308     * @return the set of relations which have to be modified according
309     * to the decisions managed by this model
310     */
311    public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) {
312        Set<Relation> ret = new HashSet<>();
313        for (Relation relation: relations) {
314            if (isChanged(relation, newPrimitive)) {
315                ret.add(relation);
316            }
317        }
318        return ret;
319    }
320}