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, 4 weeks 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.