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