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