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 default: // Do nothing 135 } 136 fireTableDataChanged(); 137 } 138 139 /** 140 * Populates the model with the members of the relation <code>relation</code> 141 * referring to <code>primitive</code>. 142 * 143 * @param relation the parent relation 144 * @param primitive the child primitive 145 */ 146 protected void populate(Relation relation, OsmPrimitive primitive) { 147 for (int i = 0; i < relation.getMembersCount(); i++) { 148 if (relation.getMember(i).refersTo(primitive)) { 149 decisions.add(new RelationMemberConflictDecision(relation, i)); 150 } 151 } 152 } 153 154 /** 155 * Populates the model with the relation members belonging to one of the relations in <code>relations</code> 156 * and referring to one of the primitives in <code>memberPrimitives</code>. 157 * 158 * @param relations the parent relations. Empty list assumed if null. 159 * @param memberPrimitives the child primitives. Empty list assumed if null. 160 */ 161 public void populate(Collection<Relation> relations, Collection<? extends OsmPrimitive> memberPrimitives) { 162 decisions.clear(); 163 relations = relations == null ? Collections.<Relation>emptyList() : relations; 164 memberPrimitives = memberPrimitives == null ? new LinkedList<OsmPrimitive>() : memberPrimitives; 165 for (Relation r : relations) { 166 for (OsmPrimitive p: memberPrimitives) { 167 populate(r, p); 168 } 169 } 170 this.relations = relations; 171 this.primitives = memberPrimitives; 172 refresh(); 173 } 174 175 /** 176 * Populates the model with the relation members represented as a collection of 177 * {@link RelationToChildReference}s. 178 * 179 * @param references the references. Empty list assumed if null. 180 */ 181 public void populate(Collection<RelationToChildReference> references) { 182 references = references == null ? new LinkedList<RelationToChildReference>() : references; 183 decisions.clear(); 184 this.relations = new HashSet<>(references.size()); 185 final Collection<OsmPrimitive> primitives = new HashSet<>(); 186 for (RelationToChildReference reference: references) { 187 decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition())); 188 relations.add(reference.getParent()); 189 primitives.add(reference.getChild()); 190 } 191 this.primitives = primitives; 192 refresh(); 193 } 194 195 /** 196 * Prepare the default decisions for the current model. 197 * 198 * Keep/delete decisions are made if every member has the same role and the members are in consecutive order within the relation. 199 * For multiple occurrences those conditions are tested stepwise for each occurrence. 200 */ 201 public void prepareDefaultRelationDecisions() { 202 203 if (Utils.forAll(primitives, OsmPrimitive.nodePredicate)) { 204 final Collection<OsmPrimitive> primitivesInDecisions = new HashSet<>(); 205 for (final RelationMemberConflictDecision i : decisions) { 206 primitivesInDecisions.add(i.getOriginalPrimitive()); 207 } 208 if (primitivesInDecisions.size() == 1) { 209 for (final RelationMemberConflictDecision i : decisions) { 210 i.decide(RelationMemberConflictDecisionType.KEEP); 211 } 212 refresh(); 213 return; 214 } 215 } 216 217 for (final Relation relation : relations) { 218 final Map<OsmPrimitive, List<RelationMemberConflictDecision>> decisionsByPrimitive = new LinkedHashMap<>(primitives.size(), 1); 219 for (final RelationMemberConflictDecision decision : decisions) { 220 if (decision.getRelation() == relation) { 221 final OsmPrimitive primitive = decision.getOriginalPrimitive(); 222 if (!decisionsByPrimitive.containsKey(primitive)) { 223 decisionsByPrimitive.put(primitive, new ArrayList<RelationMemberConflictDecision>()); 224 } 225 decisionsByPrimitive.get(primitive).add(decision); 226 } 227 } 228 229 //noinspection StatementWithEmptyBody 230 if (!decisionsByPrimitive.keySet().containsAll(primitives)) { 231 // some primitives are not part of the relation, leave undecided 232 } else { 233 final Collection<Iterator<RelationMemberConflictDecision>> iterators = new ArrayList<>(primitives.size()); 234 for (final Collection<RelationMemberConflictDecision> i : decisionsByPrimitive.values()) { 235 iterators.add(i.iterator()); 236 } 237 while (Utils.forAll(iterators, new Predicate<Iterator<RelationMemberConflictDecision>>() { 238 @Override 239 public boolean evaluate(Iterator<RelationMemberConflictDecision> it) { 240 return it.hasNext(); 241 } 242 })) { 243 final List<RelationMemberConflictDecision> decisions = new ArrayList<>(); 244 final Collection<String> roles = new HashSet<>(); 245 final Collection<Integer> indices = new TreeSet<>(); 246 for (Iterator<RelationMemberConflictDecision> it : iterators) { 247 final RelationMemberConflictDecision decision = it.next(); 248 decisions.add(decision); 249 roles.add(decision.getRole()); 250 indices.add(decision.getPos()); 251 } 252 if (roles.size() != 1) { 253 // roles to not patch, leave undecided 254 continue; 255 } else if (!isCollectionOfConsecutiveNumbers(indices)) { 256 // not consecutive members in relation, leave undecided 257 continue; 258 } 259 decisions.get(0).decide(RelationMemberConflictDecisionType.KEEP); 260 for (RelationMemberConflictDecision decision : decisions.subList(1, decisions.size())) { 261 decision.decide(RelationMemberConflictDecisionType.REMOVE); 262 } 263 } 264 } 265 } 266 267 refresh(); 268 } 269 270 static boolean isCollectionOfConsecutiveNumbers(Collection<Integer> numbers) { 271 if (numbers.isEmpty()) { 272 return true; 273 } 274 final Iterator<Integer> it = numbers.iterator(); 275 Integer previousValue = it.next(); 276 while (it.hasNext()) { 277 final Integer i = it.next(); 278 if (previousValue + 1 != i) { 279 return false; 280 } 281 previousValue = i; 282 } 283 return true; 284 } 285 286 /** 287 * Replies the decision at position <code>row</code> 288 * 289 * @param row position 290 * @return the decision at position <code>row</code> 291 */ 292 public RelationMemberConflictDecision getDecision(int row) { 293 return decisions.get(row); 294 } 295 296 /** 297 * Replies the number of decisions managed by this model 298 * 299 * @return the number of decisions managed by this model 300 */ 301 public int getNumDecisions() { 302 return decisions == null /* accessed via super constructor */ ? 0 : decisions.size(); 303 } 304 305 /** 306 * Refreshes the model state. Invoke this method to trigger necessary change 307 * events after an update of the model data. 308 * 309 */ 310 public void refresh() { 311 updateNumConflicts(); 312 GuiHelper.runInEDTAndWait(new Runnable() { 313 @Override public void run() { 314 fireTableDataChanged(); 315 } 316 }); 317 } 318 319 /** 320 * Apply a role to all member managed by this model. 321 * 322 * @param role the role. Empty string assumed if null. 323 */ 324 public void applyRole(String role) { 325 role = role == null ? "" : role; 326 for (RelationMemberConflictDecision decision : decisions) { 327 decision.setRole(role); 328 } 329 refresh(); 330 } 331 332 protected RelationMemberConflictDecision getDecision(Relation relation, int pos) { 333 for (RelationMemberConflictDecision decision: decisions) { 334 if (decision.matches(relation, pos)) return decision; 335 } 336 return null; 337 } 338 339 protected Command buildResolveCommand(Relation relation, OsmPrimitive newPrimitive) { 340 final Relation modifiedRelation = new Relation(relation); 341 modifiedRelation.setMembers(null); 342 boolean isChanged = false; 343 for (int i = 0; i < relation.getMembersCount(); i++) { 344 final RelationMember member = relation.getMember(i); 345 RelationMemberConflictDecision decision = getDecision(relation, i); 346 if (decision == null) { 347 modifiedRelation.addMember(member); 348 } else { 349 switch(decision.getDecision()) { 350 case KEEP: 351 final RelationMember newMember = new RelationMember(decision.getRole(), newPrimitive); 352 modifiedRelation.addMember(newMember); 353 isChanged |= !member.equals(newMember); 354 break; 355 case REMOVE: 356 isChanged = true; 357 // do nothing 358 break; 359 case UNDECIDED: 360 // FIXME: this is an error 361 break; 362 } 363 } 364 } 365 if (isChanged) 366 return new ChangeCommand(relation, modifiedRelation); 367 return null; 368 } 369 370 /** 371 * Builds a collection of commands executing the decisions made in this model. 372 * 373 * @param newPrimitive the primitive which members shall refer to 374 * @return a list of commands 375 */ 376 public List<Command> buildResolutionCommands(OsmPrimitive newPrimitive) { 377 List<Command> command = new LinkedList<>(); 378 for (Relation relation : relations) { 379 Command cmd = buildResolveCommand(relation, newPrimitive); 380 if (cmd != null) { 381 command.add(cmd); 382 } 383 } 384 return command; 385 } 386 387 protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) { 388 for (int i = 0; i < relation.getMembersCount(); i++) { 389 RelationMemberConflictDecision decision = getDecision(relation, i); 390 if (decision == null) { 391 continue; 392 } 393 switch(decision.getDecision()) { 394 case REMOVE: return true; 395 case KEEP: 396 if (!relation.getMember(i).getRole().equals(decision.getRole())) 397 return true; 398 if (relation.getMember(i).getMember() != newPrimitive) 399 return true; 400 case UNDECIDED: 401 // FIXME: handle error 402 } 403 } 404 return false; 405 } 406 407 /** 408 * Replies the set of relations which have to be modified according 409 * to the decisions managed by this model. 410 * 411 * @param newPrimitive the primitive which members shall refer to 412 * 413 * @return the set of relations which have to be modified according 414 * to the decisions managed by this model 415 */ 416 public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) { 417 Set<Relation> ret = new HashSet<>(); 418 for (Relation relation: relations) { 419 if (isChanged(relation, newPrimitive)) { 420 ret.add(relation); 421 } 422 } 423 return ret; 424 } 425}