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.Collections; 009import java.util.HashSet; 010import java.util.Iterator; 011import java.util.LinkedHashMap; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Map; 015import java.util.Set; 016import java.util.TreeSet; 017 018import javax.swing.table.DefaultTableModel; 019 020import org.openstreetmap.josm.command.ChangeCommand; 021import org.openstreetmap.josm.command.Command; 022import org.openstreetmap.josm.data.osm.OsmPrimitive; 023import org.openstreetmap.josm.data.osm.Relation; 024import org.openstreetmap.josm.data.osm.RelationMember; 025import org.openstreetmap.josm.data.osm.RelationToChildReference; 026import org.openstreetmap.josm.gui.util.GuiHelper; 027import org.openstreetmap.josm.tools.Predicate; 028import org.openstreetmap.josm.tools.Utils; 029 030/** 031 * This model manages a list of conflicting relation members. 032 * 033 * It can be used as {@link javax.swing.table.TableModel}. 034 */ 035public class RelationMemberConflictResolverModel extends DefaultTableModel { 036 /** the property name for the number conflicts managed by this model */ 037 public static final String NUM_CONFLICTS_PROP = RelationMemberConflictResolverModel.class.getName() + ".numConflicts"; 038 039 /** the list of conflict decisions */ 040 protected final transient List<RelationMemberConflictDecision> decisions; 041 /** the collection of relations for which we manage conflicts */ 042 protected transient Collection<Relation> relations; 043 /** the collection of primitives for which we manage conflicts */ 044 protected transient Collection<? extends OsmPrimitive> primitives; 045 /** the number of conflicts */ 046 private int numConflicts; 047 private final PropertyChangeSupport support; 048 049 /** 050 * Replies true if each {@link MultiValueResolutionDecision} is decided. 051 * 052 * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise 053 */ 054 public boolean isResolvedCompletely() { 055 return numConflicts == 0; 056 } 057 058 /** 059 * Replies the current number of conflicts 060 * 061 * @return the current number of conflicts 062 */ 063 public int getNumConflicts() { 064 return numConflicts; 065 } 066 067 /** 068 * Updates the current number of conflicts from list of decisions and emits 069 * a property change event if necessary. 070 * 071 */ 072 protected void updateNumConflicts() { 073 int count = 0; 074 for (RelationMemberConflictDecision decision: decisions) { 075 if (!decision.isDecided()) { 076 count++; 077 } 078 } 079 int oldValue = numConflicts; 080 numConflicts = count; 081 if (numConflicts != oldValue) { 082 support.firePropertyChange(getProperty(), oldValue, numConflicts); 083 } 084 } 085 086 protected String getProperty() { 087 return NUM_CONFLICTS_PROP; 088 } 089 090 public void addPropertyChangeListener(PropertyChangeListener l) { 091 support.addPropertyChangeListener(l); 092 } 093 094 public void removePropertyChangeListener(PropertyChangeListener l) { 095 support.removePropertyChangeListener(l); 096 } 097 098 public RelationMemberConflictResolverModel() { 099 decisions = new ArrayList<>(); 100 support = new PropertyChangeSupport(this); 101 } 102 103 @Override 104 public int getRowCount() { 105 return getNumDecisions(); 106 } 107 108 @Override 109 public Object getValueAt(int row, int column) { 110 if (decisions == null) return null; 111 112 RelationMemberConflictDecision d = decisions.get(row); 113 switch(column) { 114 case 0: /* relation */ return d.getRelation(); 115 case 1: /* pos */ return Integer.toString(d.getPos() + 1); // position in "user space" starting at 1 116 case 2: /* role */ return d.getRole(); 117 case 3: /* original */ return d.getOriginalPrimitive(); 118 case 4: /* decision */ return d.getDecision(); 119 } 120 return null; 121 } 122 123 @Override 124 public void setValueAt(Object value, int row, int column) { 125 RelationMemberConflictDecision d = decisions.get(row); 126 switch(column) { 127 case 2: /* role */ 128 d.setRole((String) value); 129 break; 130 case 4: /* decision */ 131 d.decide((RelationMemberConflictDecisionType) value); 132 refresh(); 133 break; 134 } 135 fireTableDataChanged(); 136 } 137 138 /** 139 * Populates the model with the members of the relation <code>relation</code> 140 * referring to <code>primitive</code>. 141 * 142 * @param relation the parent relation 143 * @param primitive the child primitive 144 */ 145 protected void populate(Relation relation, OsmPrimitive primitive) { 146 for (int i = 0; i < relation.getMembersCount(); i++) { 147 if (relation.getMember(i).refersTo(primitive)) { 148 decisions.add(new RelationMemberConflictDecision(relation, i)); 149 } 150 } 151 } 152 153 /** 154 * Populates the model with the relation members belonging to one of the relations in <code>relations</code> 155 * and referring to one of the primitives in <code>memberPrimitives</code>. 156 * 157 * @param relations the parent relations. Empty list assumed if null. 158 * @param memberPrimitives the child primitives. Empty list assumed if null. 159 */ 160 public void populate(Collection<Relation> relations, Collection<? extends OsmPrimitive> memberPrimitives) { 161 decisions.clear(); 162 relations = relations == null ? Collections.<Relation>emptyList() : relations; 163 memberPrimitives = memberPrimitives == null ? new LinkedList<OsmPrimitive>() : memberPrimitives; 164 for (Relation r : relations) { 165 for (OsmPrimitive p: memberPrimitives) { 166 populate(r, p); 167 } 168 } 169 this.relations = relations; 170 this.primitives = memberPrimitives; 171 refresh(); 172 } 173 174 /** 175 * Populates the model with the relation members represented as a collection of 176 * {@link RelationToChildReference}s. 177 * 178 * @param references the references. Empty list assumed if null. 179 */ 180 public void populate(Collection<RelationToChildReference> references) { 181 references = references == null ? new LinkedList<RelationToChildReference>() : references; 182 decisions.clear(); 183 this.relations = new HashSet<>(references.size()); 184 final Collection<OsmPrimitive> primitives = new HashSet<>(); 185 for (RelationToChildReference reference: references) { 186 decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition())); 187 relations.add(reference.getParent()); 188 primitives.add(reference.getChild()); 189 } 190 this.primitives = primitives; 191 refresh(); 192 } 193 194 /** 195 * Prepare the default decisions for the current model. 196 * 197 * Keep/delete decisions are made if every member has the same role and the members are in consecutive order within the relation. 198 * For multiple occurrences those conditions are tested stepwise for each occurrence. 199 */ 200 public void prepareDefaultRelationDecisions() { 201 202 for (final Relation relation : relations) { 203 final Map<OsmPrimitive, List<RelationMemberConflictDecision>> decisionsByPrimitive = new LinkedHashMap<>(primitives.size(), 1); 204 for (final RelationMemberConflictDecision decision : decisions) { 205 if (decision.getRelation() == relation) { 206 final OsmPrimitive primitive = decision.getOriginalPrimitive(); 207 if (!decisionsByPrimitive.containsKey(primitive)) { 208 decisionsByPrimitive.put(primitive, new ArrayList<RelationMemberConflictDecision>()); 209 } 210 decisionsByPrimitive.get(primitive).add(decision); 211 } 212 } 213 214 //noinspection StatementWithEmptyBody 215 if (!decisionsByPrimitive.keySet().containsAll(primitives)) { 216 // some primitives are not part of the relation, leave undecided 217 } else { 218 final Collection<Iterator<RelationMemberConflictDecision>> iterators = new ArrayList<>(primitives.size()); 219 for (final Collection<RelationMemberConflictDecision> i : decisionsByPrimitive.values()) { 220 iterators.add(i.iterator()); 221 } 222 while (Utils.forAll(iterators, new Predicate<Iterator<RelationMemberConflictDecision>>() { 223 @Override 224 public boolean evaluate(Iterator<RelationMemberConflictDecision> it) { 225 return it.hasNext(); 226 } 227 })) { 228 final List<RelationMemberConflictDecision> decisions = new ArrayList<>(); 229 final Collection<String> roles = new HashSet<>(); 230 final Collection<Integer> indices = new TreeSet<>(); 231 for (Iterator<RelationMemberConflictDecision> it : iterators) { 232 final RelationMemberConflictDecision decision = it.next(); 233 decisions.add(decision); 234 roles.add(decision.getRole()); 235 indices.add(decision.getPos()); 236 } 237 if (roles.size() != 1) { 238 // roles to not patch, leave undecided 239 continue; 240 } else if (!isCollectionOfConsecutiveNumbers(indices)) { 241 // not consecutive members in relation, leave undecided 242 continue; 243 } 244 decisions.get(0).decide(RelationMemberConflictDecisionType.KEEP); 245 for (RelationMemberConflictDecision decision : decisions.subList(1, decisions.size())) { 246 decision.decide(RelationMemberConflictDecisionType.REMOVE); 247 } 248 } 249 } 250 } 251 252 refresh(); 253 } 254 255 static boolean isCollectionOfConsecutiveNumbers(Collection<Integer> numbers) { 256 if (numbers.isEmpty()) { 257 return true; 258 } 259 final Iterator<Integer> it = numbers.iterator(); 260 Integer previousValue = it.next(); 261 while (it.hasNext()) { 262 final Integer i = it.next(); 263 if (previousValue + 1 != i) { 264 return false; 265 } 266 previousValue = i; 267 } 268 return true; 269 } 270 271 /** 272 * Replies the decision at position <code>row</code> 273 * 274 * @param row position 275 * @return the decision at position <code>row</code> 276 */ 277 public RelationMemberConflictDecision getDecision(int row) { 278 return decisions.get(row); 279 } 280 281 /** 282 * Replies the number of decisions managed by this model 283 * 284 * @return the number of decisions managed by this model 285 */ 286 public int getNumDecisions() { 287 return decisions == null /* accessed via super constructor */ ? 0 : decisions.size(); 288 } 289 290 /** 291 * Refreshes the model state. Invoke this method to trigger necessary change 292 * events after an update of the model data. 293 * 294 */ 295 public void refresh() { 296 updateNumConflicts(); 297 GuiHelper.runInEDTAndWait(new Runnable() { 298 @Override public void run() { 299 fireTableDataChanged(); 300 } 301 }); 302 } 303 304 /** 305 * Apply a role to all member managed by this model. 306 * 307 * @param role the role. Empty string assumed if null. 308 */ 309 public void applyRole(String role) { 310 role = role == null ? "" : role; 311 for (RelationMemberConflictDecision decision : decisions) { 312 decision.setRole(role); 313 } 314 refresh(); 315 } 316 317 protected RelationMemberConflictDecision getDecision(Relation relation, int pos) { 318 for (RelationMemberConflictDecision decision: decisions) { 319 if (decision.matches(relation, pos)) return decision; 320 } 321 return null; 322 } 323 324 protected Command buildResolveCommand(Relation relation, OsmPrimitive newPrimitive) { 325 final Relation modifiedRelation = new Relation(relation); 326 modifiedRelation.setMembers(null); 327 boolean isChanged = false; 328 for (int i = 0; i < relation.getMembersCount(); i++) { 329 final RelationMember member = relation.getMember(i); 330 RelationMemberConflictDecision decision = getDecision(relation, i); 331 if (decision == null) { 332 modifiedRelation.addMember(member); 333 } else { 334 switch(decision.getDecision()) { 335 case KEEP: 336 final RelationMember newMember = new RelationMember(decision.getRole(), newPrimitive); 337 modifiedRelation.addMember(newMember); 338 isChanged |= !member.equals(newMember); 339 break; 340 case REMOVE: 341 isChanged = true; 342 // do nothing 343 break; 344 case UNDECIDED: 345 // FIXME: this is an error 346 break; 347 } 348 } 349 } 350 if (isChanged) 351 return new ChangeCommand(relation, modifiedRelation); 352 return null; 353 } 354 355 /** 356 * Builds a collection of commands executing the decisions made in this model. 357 * 358 * @param newPrimitive the primitive which members shall refer to 359 * @return a list of commands 360 */ 361 public List<Command> buildResolutionCommands(OsmPrimitive newPrimitive) { 362 List<Command> command = new LinkedList<>(); 363 for (Relation relation : relations) { 364 Command cmd = buildResolveCommand(relation, newPrimitive); 365 if (cmd != null) { 366 command.add(cmd); 367 } 368 } 369 return command; 370 } 371 372 protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) { 373 for (int i = 0; i < relation.getMembersCount(); i++) { 374 RelationMemberConflictDecision decision = getDecision(relation, i); 375 if (decision == null) { 376 continue; 377 } 378 switch(decision.getDecision()) { 379 case REMOVE: return true; 380 case KEEP: 381 if (!relation.getMember(i).getRole().equals(decision.getRole())) 382 return true; 383 if (relation.getMember(i).getMember() != newPrimitive) 384 return true; 385 case UNDECIDED: 386 // FIXME: handle error 387 } 388 } 389 return false; 390 } 391 392 /** 393 * Replies the set of relations which have to be modified according 394 * to the decisions managed by this model. 395 * 396 * @param newPrimitive the primitive which members shall refer to 397 * 398 * @return the set of relations which have to be modified according 399 * to the decisions managed by this model 400 */ 401 public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) { 402 Set<Relation> ret = new HashSet<>(); 403 for (Relation relation: relations) { 404 if (isChanged(relation, newPrimitive)) { 405 ret.add(relation); 406 } 407 } 408 return ret; 409 } 410}