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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 14 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 16.8 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.Objects;
16import java.util.Set;
17import java.util.TreeSet;
18import java.util.stream.Collectors;
19
20import javax.swing.table.DefaultTableModel;
21
22import org.openstreetmap.josm.command.ChangeMembersCommand;
23import org.openstreetmap.josm.command.Command;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.RelationMember;
28import org.openstreetmap.josm.data.osm.RelationToChildReference;
29import org.openstreetmap.josm.gui.util.GuiHelper;
30
31/**
32 * This model manages a list of conflicting relation members.
33 *
34 * It can be used as {@link javax.swing.table.TableModel}.
35 */
36public class RelationMemberConflictResolverModel extends DefaultTableModel {
37 /** the property name for the number conflicts managed by this model */
38 public static final String NUM_CONFLICTS_PROP = RelationMemberConflictResolverModel.class.getName() + ".numConflicts";
39
40 /** the list of conflict decisions */
41 protected final transient List<RelationMemberConflictDecision> decisions;
42 /** the collection of relations for which we manage conflicts */
43 protected transient Collection<Relation> relations;
44 /** the collection of primitives for which we manage conflicts */
45 protected transient Collection<? extends OsmPrimitive> primitives;
46 /** the number of conflicts */
47 private int numConflicts;
48 private final PropertyChangeSupport support;
49
50 /**
51 * Replies true if each {@link MultiValueResolutionDecision} is decided.
52 *
53 * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise
54 */
55 public boolean isResolvedCompletely() {
56 return numConflicts == 0;
57 }
58
59 /**
60 * Replies the current number of conflicts
61 *
62 * @return the current number of conflicts
63 */
64 public int getNumConflicts() {
65 return numConflicts;
66 }
67
68 /**
69 * Updates the current number of conflicts from list of decisions and emits
70 * a property change event if necessary.
71 *
72 */
73 protected void updateNumConflicts() {
74 int oldValue = numConflicts;
75 numConflicts = (int) decisions.stream().filter(decision -> !decision.isDecided()).count();
76 if (numConflicts != oldValue) {
77 support.firePropertyChange(getProperty(), oldValue, numConflicts);
78 }
79 }
80
81 protected String getProperty() {
82 return NUM_CONFLICTS_PROP;
83 }
84
85 public void addPropertyChangeListener(PropertyChangeListener l) {
86 support.addPropertyChangeListener(l);
87 }
88
89 public void removePropertyChangeListener(PropertyChangeListener l) {
90 support.removePropertyChangeListener(l);
91 }
92
93 public RelationMemberConflictResolverModel() {
94 decisions = new ArrayList<>();
95 support = new PropertyChangeSupport(this);
96 }
97
98 @Override
99 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 == d.getDecision();
114 case 5: /* decision remove */ return RelationMemberConflictDecisionType.REMOVE == 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> newPrimitives = new HashSet<>();
203 for (RelationToChildReference reference: references) {
204 decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition()));
205 relations.add(reference.getParent());
206 newPrimitives.add(reference.getChild());
207 }
208 this.primitives = newPrimitives;
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<>());
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> conflictDecisions = 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 conflictDecisions.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 conflictDecisions.get(0).decide(RelationMemberConflictDecisionType.KEEP);
279 for (RelationMemberConflictDecision decision : conflictDecisions.subList(1, conflictDecisions.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}
Note: See TracBrowser for help on using the repository browser.