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}