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