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