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 }