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