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