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 keep */ return RelationMemberConflictDecisionType.KEEP.equals(d.getDecision());
118        case 5: /* decision remove */ return RelationMemberConflictDecisionType.REMOVE.equals(d.getDecision());
119        }
120        return null;
121    }
122
123    @Override
124    public void setValueAt(Object value, int row, int column) {
125        RelationMemberConflictDecision d = decisions.get(row);
126        switch(column) {
127        case 2: /* role */
128            d.setRole((String) value);
129            break;
130        case 4: /* decision keep */
131            if (Boolean.TRUE.equals(value)) {
132                d.decide(RelationMemberConflictDecisionType.KEEP);
133                refresh(false);
134            }
135            break;
136        case 5: /* decision remove */
137            if (Boolean.TRUE.equals(value)) {
138                d.decide(RelationMemberConflictDecisionType.REMOVE);
139                refresh(false);
140            }
141            break;
142        default: // Do nothing
143        }
144        fireTableDataChanged();
145    }
146
147    /**
148     * Populates the model with the members of the relation <code>relation</code>
149     * referring to <code>primitive</code>.
150     *
151     * @param relation the parent relation
152     * @param primitive the child primitive
153     */
154    protected void populate(Relation relation, OsmPrimitive primitive) {
155        for (int i = 0; i < relation.getMembersCount(); i++) {
156            if (relation.getMember(i).refersTo(primitive)) {
157                decisions.add(new RelationMemberConflictDecision(relation, i));
158            }
159        }
160    }
161
162    /**
163     * Populates the model with the relation members belonging to one of the relations in <code>relations</code>
164     * and referring to one of the primitives in <code>memberPrimitives</code>.
165     *
166     * @param relations  the parent relations. Empty list assumed if null.
167     * @param memberPrimitives the child primitives. Empty list assumed if null.
168     */
169    public void populate(Collection<Relation> relations, Collection<? extends OsmPrimitive> memberPrimitives) {
170        populate(relations, memberPrimitives, true);
171    }
172
173    /**
174     * Populates the model with the relation members belonging to one of the relations in <code>relations</code>
175     * and referring to one of the primitives in <code>memberPrimitives</code>.
176     *
177     * @param relations  the parent relations. Empty list assumed if null.
178     * @param memberPrimitives the child primitives. Empty list assumed if null.
179     * @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
180     * @since 11626
181     */
182    void populate(Collection<Relation> relations, Collection<? extends OsmPrimitive> memberPrimitives, boolean fireEvent) {
183        decisions.clear();
184        relations = relations == null ? Collections.<Relation>emptyList() : relations;
185        memberPrimitives = memberPrimitives == null ? new LinkedList<>() : memberPrimitives;
186        for (Relation r : relations) {
187            for (OsmPrimitive p: memberPrimitives) {
188                populate(r, p);
189            }
190        }
191        this.relations = relations;
192        this.primitives = memberPrimitives;
193        refresh(fireEvent);
194    }
195
196    /**
197     * Populates the model with the relation members represented as a collection of
198     * {@link RelationToChildReference}s.
199     *
200     * @param references the references. Empty list assumed if null.
201     */
202    public void populate(Collection<RelationToChildReference> references) {
203        references = references == null ? new LinkedList<>() : references;
204        decisions.clear();
205        this.relations = new HashSet<>(references.size());
206        final Collection<OsmPrimitive> primitives = new HashSet<>();
207        for (RelationToChildReference reference: references) {
208            decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition()));
209            relations.add(reference.getParent());
210            primitives.add(reference.getChild());
211        }
212        this.primitives = primitives;
213        refresh();
214    }
215
216    /**
217     * Prepare the default decisions for the current model.
218     *
219     * Keep/delete decisions are made if every member has the same role and the members are in consecutive order within the relation.
220     * For multiple occurrences those conditions are tested stepwise for each occurrence.
221     */
222    public void prepareDefaultRelationDecisions() {
223        prepareDefaultRelationDecisions(true);
224    }
225
226    /**
227     * Prepare the default decisions for the current model.
228     *
229     * Keep/delete decisions are made if every member has the same role and the members are in consecutive order within the relation.
230     * For multiple occurrences those conditions are tested stepwise for each occurrence.
231     *
232     * @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
233     * @since 11626
234     */
235    void prepareDefaultRelationDecisions(boolean fireEvent) {
236        if (primitives.stream().allMatch(Node.class::isInstance)) {
237            final Collection<OsmPrimitive> primitivesInDecisions = new HashSet<>();
238            for (final RelationMemberConflictDecision i : decisions) {
239                primitivesInDecisions.add(i.getOriginalPrimitive());
240            }
241            if (primitivesInDecisions.size() == 1) {
242                for (final RelationMemberConflictDecision i : decisions) {
243                    i.decide(RelationMemberConflictDecisionType.KEEP);
244                }
245                refresh();
246                return;
247            }
248        }
249
250        for (final Relation relation : relations) {
251            final Map<OsmPrimitive, List<RelationMemberConflictDecision>> decisionsByPrimitive = new LinkedHashMap<>(primitives.size(), 1);
252            for (final RelationMemberConflictDecision decision : decisions) {
253                if (decision.getRelation() == relation) {
254                    final OsmPrimitive primitive = decision.getOriginalPrimitive();
255                    if (!decisionsByPrimitive.containsKey(primitive)) {
256                        decisionsByPrimitive.put(primitive, new ArrayList<RelationMemberConflictDecision>());
257                    }
258                    decisionsByPrimitive.get(primitive).add(decision);
259                }
260            }
261
262            //noinspection StatementWithEmptyBody
263            if (!decisionsByPrimitive.keySet().containsAll(primitives)) {
264                // some primitives are not part of the relation, leave undecided
265            } else {
266                final Collection<Iterator<RelationMemberConflictDecision>> iterators = new ArrayList<>(primitives.size());
267                for (final Collection<RelationMemberConflictDecision> i : decisionsByPrimitive.values()) {
268                    iterators.add(i.iterator());
269                }
270                while (iterators.stream().allMatch(Iterator::hasNext)) {
271                    final List<RelationMemberConflictDecision> decisions = new ArrayList<>();
272                    final Collection<String> roles = new HashSet<>();
273                    final Collection<Integer> indices = new TreeSet<>();
274                    for (Iterator<RelationMemberConflictDecision> it : iterators) {
275                        final RelationMemberConflictDecision decision = it.next();
276                        decisions.add(decision);
277                        roles.add(decision.getRole());
278                        indices.add(decision.getPos());
279                    }
280                    if (roles.size() != 1 || !isCollectionOfConsecutiveNumbers(indices)) {
281                        // roles do not match or not consecutive members in relation, leave undecided
282                        continue;
283                    }
284                    decisions.get(0).decide(RelationMemberConflictDecisionType.KEEP);
285                    for (RelationMemberConflictDecision decision : decisions.subList(1, decisions.size())) {
286                        decision.decide(RelationMemberConflictDecisionType.REMOVE);
287                    }
288                }
289            }
290        }
291
292        refresh(fireEvent);
293    }
294
295    static boolean isCollectionOfConsecutiveNumbers(Collection<Integer> numbers) {
296        if (numbers.isEmpty()) {
297            return true;
298        }
299        final Iterator<Integer> it = numbers.iterator();
300        Integer previousValue = it.next();
301        while (it.hasNext()) {
302            final Integer i = it.next();
303            if (previousValue + 1 != i) {
304                return false;
305            }
306            previousValue = i;
307        }
308        return true;
309    }
310
311    /**
312     * Replies the decision at position <code>row</code>
313     *
314     * @param row position
315     * @return the decision at position <code>row</code>
316     */
317    public RelationMemberConflictDecision getDecision(int row) {
318        return decisions.get(row);
319    }
320
321    /**
322     * Replies the number of decisions managed by this model
323     *
324     * @return the number of decisions managed by this model
325     */
326    public int getNumDecisions() {
327        return decisions == null /* accessed via super constructor */ ? 0 : decisions.size();
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     *
334     */
335    public void refresh() {
336        refresh(true);
337    }
338
339    /**
340     * Refreshes the model state. Invoke this method to trigger necessary change
341     * events after an update of the model data.
342     * @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
343     * @since 11626
344     */
345    void refresh(boolean fireEvent) {
346        updateNumConflicts();
347        if (fireEvent) {
348            GuiHelper.runInEDTAndWait(this::fireTableDataChanged);
349        }
350    }
351
352    /**
353     * Apply a role to all member managed by this model.
354     *
355     * @param role the role. Empty string assumed if null.
356     */
357    public void applyRole(String role) {
358        role = role == null ? "" : role;
359        for (RelationMemberConflictDecision decision : decisions) {
360            decision.setRole(role);
361        }
362        refresh();
363    }
364
365    protected RelationMemberConflictDecision getDecision(Relation relation, int pos) {
366        for (RelationMemberConflictDecision decision: decisions) {
367            if (decision.matches(relation, pos)) return decision;
368        }
369        return null;
370    }
371
372    protected Command buildResolveCommand(Relation relation, OsmPrimitive newPrimitive) {
373        final Relation modifiedRelation = new Relation(relation);
374        modifiedRelation.setMembers(null);
375        boolean isChanged = false;
376        for (int i = 0; i < relation.getMembersCount(); i++) {
377            final RelationMember member = relation.getMember(i);
378            RelationMemberConflictDecision decision = getDecision(relation, i);
379            if (decision == null) {
380                modifiedRelation.addMember(member);
381            } else {
382                switch(decision.getDecision()) {
383                case KEEP:
384                    final RelationMember newMember = new RelationMember(decision.getRole(), newPrimitive);
385                    modifiedRelation.addMember(newMember);
386                    isChanged |= !member.equals(newMember);
387                    break;
388                case REMOVE:
389                    isChanged = true;
390                    // do nothing
391                    break;
392                case UNDECIDED:
393                    // FIXME: this is an error
394                    break;
395                }
396            }
397        }
398        if (isChanged)
399            return new ChangeCommand(relation, modifiedRelation);
400        return null;
401    }
402
403    /**
404     * Builds a collection of commands executing the decisions made in this model.
405     *
406     * @param newPrimitive the primitive which members shall refer to
407     * @return a list of commands
408     */
409    public List<Command> buildResolutionCommands(OsmPrimitive newPrimitive) {
410        List<Command> command = new LinkedList<>();
411        for (Relation relation : relations) {
412            Command cmd = buildResolveCommand(relation, newPrimitive);
413            if (cmd != null) {
414                command.add(cmd);
415            }
416        }
417        return command;
418    }
419
420    protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) {
421        for (int i = 0; i < relation.getMembersCount(); i++) {
422            RelationMemberConflictDecision decision = getDecision(relation, i);
423            if (decision == null) {
424                continue;
425            }
426            switch(decision.getDecision()) {
427            case REMOVE: return true;
428            case KEEP:
429                if (!relation.getMember(i).getRole().equals(decision.getRole()))
430                    return true;
431                if (relation.getMember(i).getMember() != newPrimitive)
432                    return true;
433                break;
434            case UNDECIDED:
435                // FIXME: handle error
436            }
437        }
438        return false;
439    }
440
441    /**
442     * Replies the set of relations which have to be modified according
443     * to the decisions managed by this model.
444     *
445     * @param newPrimitive the primitive which members shall refer to
446     *
447     * @return the set of relations which have to be modified according
448     * to the decisions managed by this model
449     */
450    public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) {
451        Set<Relation> ret = new HashSet<>();
452        for (Relation relation: relations) {
453            if (isChanged(relation, newPrimitive)) {
454                ret.add(relation);
455            }
456        }
457        return ret;
458    }
459}