source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/tags/RelationMemberConflictResolverModel.java@ 11627

Last change on this file since 11627 was 11627, checked in by Don-vip, 7 years ago

fix #3346 - improve drastically the performance of fixing duplicate nodes by:

  • caching data sources area computation
  • moving layer invalidation from UndoRedoHandler.addNoRedraw to UndoRedoHandler.add
  • avoiding any EDT call when building tag conflict dialog if it's not meant to be displayed
  • Property svn:eol-style set to native
File size: 16.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.tags;
3
4import java.beans.PropertyChangeListener;
5import java.beans.PropertyChangeSupport;
6import java.util.ArrayList;
7import java.util.Collection;
8import java.util.Collections;
9import java.util.HashSet;
10import java.util.Iterator;
11import java.util.LinkedHashMap;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Map;
15import java.util.Set;
16import java.util.TreeSet;
17
18import javax.swing.table.DefaultTableModel;
19
20import org.openstreetmap.josm.command.ChangeCommand;
21import org.openstreetmap.josm.command.Command;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.Relation;
25import org.openstreetmap.josm.data.osm.RelationMember;
26import org.openstreetmap.josm.data.osm.RelationToChildReference;
27import org.openstreetmap.josm.gui.util.GuiHelper;
28
29/**
30 * This model manages a list of conflicting relation members.
31 *
32 * It can be used as {@link javax.swing.table.TableModel}.
33 */
34public class RelationMemberConflictResolverModel extends DefaultTableModel {
35 /** the property name for the number conflicts managed by this model */
36 public static final String NUM_CONFLICTS_PROP = RelationMemberConflictResolverModel.class.getName() + ".numConflicts";
37
38 /** the list of conflict decisions */
39 protected final transient List<RelationMemberConflictDecision> decisions;
40 /** the collection of relations for which we manage conflicts */
41 protected transient Collection<Relation> relations;
42 /** the collection of primitives for which we manage conflicts */
43 protected transient Collection<? extends OsmPrimitive> primitives;
44 /** the number of conflicts */
45 private int numConflicts;
46 private final PropertyChangeSupport support;
47
48 /**
49 * Replies true if each {@link MultiValueResolutionDecision} is decided.
50 *
51 * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise
52 */
53 public boolean isResolvedCompletely() {
54 return numConflicts == 0;
55 }
56
57 /**
58 * Replies the current number of conflicts
59 *
60 * @return the current number of conflicts
61 */
62 public int getNumConflicts() {
63 return numConflicts;
64 }
65
66 /**
67 * Updates the current number of conflicts from list of decisions and emits
68 * a property change event if necessary.
69 *
70 */
71 protected void updateNumConflicts() {
72 int count = 0;
73 for (RelationMemberConflictDecision decision: decisions) {
74 if (!decision.isDecided()) {
75 count++;
76 }
77 }
78 int oldValue = numConflicts;
79 numConflicts = count;
80 if (numConflicts != oldValue) {
81 support.firePropertyChange(getProperty(), oldValue, numConflicts);
82 }
83 }
84
85 protected String getProperty() {
86 return NUM_CONFLICTS_PROP;
87 }
88
89 public void addPropertyChangeListener(PropertyChangeListener l) {
90 support.addPropertyChangeListener(l);
91 }
92
93 public void removePropertyChangeListener(PropertyChangeListener l) {
94 support.removePropertyChangeListener(l);
95 }
96
97 public RelationMemberConflictResolverModel() {
98 decisions = new ArrayList<>();
99 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}
Note: See TracBrowser for help on using the repository browser.