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