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