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 }