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 if (Utils.forAll(primitives, OsmPrimitive.nodePredicate)) { 203 final Collection<OsmPrimitive> primitivesInDecisions = new HashSet<>(); 204 for (final RelationMemberConflictDecision i : decisions) { 205 primitivesInDecisions.add(i.getOriginalPrimitive()); 206 } 207 if (primitivesInDecisions.size() == 1) { 208 for (final RelationMemberConflictDecision i : decisions) { 209 i.decide(RelationMemberConflictDecisionType.KEEP); 210 } 211 refresh(); 212 return; 213 } 214 } 215 216 for (final Relation relation : relations) { 217 final Map<OsmPrimitive, List<RelationMemberConflictDecision>> decisionsByPrimitive = new LinkedHashMap<>(primitives.size(), 1); 218 for (final RelationMemberConflictDecision decision : decisions) { 219 if (decision.getRelation() == relation) { 220 final OsmPrimitive primitive = decision.getOriginalPrimitive(); 221 if (!decisionsByPrimitive.containsKey(primitive)) { 222 decisionsByPrimitive.put(primitive, new ArrayList<RelationMemberConflictDecision>()); 223 } 224 decisionsByPrimitive.get(primitive).add(decision); 225 } 226 } 227 228 //noinspection StatementWithEmptyBody 229 if (!decisionsByPrimitive.keySet().containsAll(primitives)) { 230 // some primitives are not part of the relation, leave undecided 231 } else { 232 final Collection<Iterator<RelationMemberConflictDecision>> iterators = new ArrayList<>(primitives.size()); 233 for (final Collection<RelationMemberConflictDecision> i : decisionsByPrimitive.values()) { 234 iterators.add(i.iterator()); 235 } 236 while (Utils.forAll(iterators, new Predicate<Iterator<RelationMemberConflictDecision>>() { 237 @Override 238 public boolean evaluate(Iterator<RelationMemberConflictDecision> it) { 239 return it.hasNext(); 240 } 241 })) { 242 final List<RelationMemberConflictDecision> decisions = new ArrayList<>(); 243 final Collection<String> roles = new HashSet<>(); 244 final Collection<Integer> indices = new TreeSet<>(); 245 for (Iterator<RelationMemberConflictDecision> it : iterators) { 246 final RelationMemberConflictDecision decision = it.next(); 247 decisions.add(decision); 248 roles.add(decision.getRole()); 249 indices.add(decision.getPos()); 250 } 251 if (roles.size() != 1) { 252 // roles to not patch, leave undecided 253 continue; 254 } else if (!isCollectionOfConsecutiveNumbers(indices)) { 255 // not consecutive members in relation, leave undecided 256 continue; 257 } 258 decisions.get(0).decide(RelationMemberConflictDecisionType.KEEP); 259 for (RelationMemberConflictDecision decision : decisions.subList(1, decisions.size())) { 260 decision.decide(RelationMemberConflictDecisionType.REMOVE); 261 } 262 } 263 } 264 } 265 266 refresh(); 267 } 268 269 static boolean isCollectionOfConsecutiveNumbers(Collection<Integer> numbers) { 270 if (numbers.isEmpty()) { 271 return true; 272 } 273 final Iterator<Integer> it = numbers.iterator(); 274 Integer previousValue = it.next(); 275 while (it.hasNext()) { 276 final Integer i = it.next(); 277 if (previousValue + 1 != i) { 278 return false; 279 } 280 previousValue = i; 281 } 282 return true; 283 } 284 285 /** 286 * Replies the decision at position <code>row</code> 287 * 288 * @param row position 289 * @return the decision at position <code>row</code> 290 */ 291 public RelationMemberConflictDecision getDecision(int row) { 292 return decisions.get(row); 293 } 294 295 /** 296 * Replies the number of decisions managed by this model 297 * 298 * @return the number of decisions managed by this model 299 */ 300 public int getNumDecisions() { 301 return decisions == null /* accessed via super constructor */ ? 0 : decisions.size(); 302 } 303 304 /** 305 * Refreshes the model state. Invoke this method to trigger necessary change 306 * events after an update of the model data. 307 * 308 */ 309 public void refresh() { 310 updateNumConflicts(); 311 GuiHelper.runInEDTAndWait(new Runnable() { 312 @Override public void run() { 313 fireTableDataChanged(); 314 } 315 }); 316 } 317 318 /** 319 * Apply a role to all member managed by this model. 320 * 321 * @param role the role. Empty string assumed if null. 322 */ 323 public void applyRole(String role) { 324 role = role == null ? "" : role; 325 for (RelationMemberConflictDecision decision : decisions) { 326 decision.setRole(role); 327 } 328 refresh(); 329 } 330 331 protected RelationMemberConflictDecision getDecision(Relation relation, int pos) { 332 for (RelationMemberConflictDecision decision: decisions) { 333 if (decision.matches(relation, pos)) return decision; 334 } 335 return null; 336 } 337 338 protected Command buildResolveCommand(Relation relation, OsmPrimitive newPrimitive) { 339 final Relation modifiedRelation = new Relation(relation); 340 modifiedRelation.setMembers(null); 341 boolean isChanged = false; 342 for (int i = 0; i < relation.getMembersCount(); i++) { 343 final RelationMember member = relation.getMember(i); 344 RelationMemberConflictDecision decision = getDecision(relation, i); 345 if (decision == null) { 346 modifiedRelation.addMember(member); 347 } else { 348 switch(decision.getDecision()) { 349 case KEEP: 350 final RelationMember newMember = new RelationMember(decision.getRole(), newPrimitive); 351 modifiedRelation.addMember(newMember); 352 isChanged |= !member.equals(newMember); 353 break; 354 case REMOVE: 355 isChanged = true; 356 // do nothing 357 break; 358 case UNDECIDED: 359 // FIXME: this is an error 360 break; 361 } 362 } 363 } 364 if (isChanged) 365 return new ChangeCommand(relation, modifiedRelation); 366 return null; 367 } 368 369 /** 370 * Builds a collection of commands executing the decisions made in this model. 371 * 372 * @param newPrimitive the primitive which members shall refer to 373 * @return a list of commands 374 */ 375 public List<Command> buildResolutionCommands(OsmPrimitive newPrimitive) { 376 List<Command> command = new LinkedList<>(); 377 for (Relation relation : relations) { 378 Command cmd = buildResolveCommand(relation, newPrimitive); 379 if (cmd != null) { 380 command.add(cmd); 381 } 382 } 383 return command; 384 } 385 386 protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) { 387 for (int i = 0; i < relation.getMembersCount(); i++) { 388 RelationMemberConflictDecision decision = getDecision(relation, i); 389 if (decision == null) { 390 continue; 391 } 392 switch(decision.getDecision()) { 393 case REMOVE: return true; 394 case KEEP: 395 if (!relation.getMember(i).getRole().equals(decision.getRole())) 396 return true; 397 if (relation.getMember(i).getMember() != newPrimitive) 398 return true; 399 case UNDECIDED: 400 // FIXME: handle error 401 } 402 } 403 return false; 404 } 405 406 /** 407 * Replies the set of relations which have to be modified according 408 * to the decisions managed by this model. 409 * 410 * @param newPrimitive the primitive which members shall refer to 411 * 412 * @return the set of relations which have to be modified according 413 * to the decisions managed by this model 414 */ 415 public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) { 416 Set<Relation> ret = new HashSet<>(); 417 for (Relation relation: relations) { 418 if (isChanged(relation, newPrimitive)) { 419 ret.add(relation); 420 } 421 } 422 return ret; 423 } 424}