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