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 */ 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();
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        decisions.clear();
162        relations = relations == null ? Collections.<Relation>emptyList() : relations;
163        memberPrimitives = memberPrimitives == null ? new LinkedList<>() : memberPrimitives;
164        for (Relation r : relations) {
165            for (OsmPrimitive p: memberPrimitives) {
166                populate(r, p);
167            }
168        }
169        this.relations = relations;
170        this.primitives = memberPrimitives;
171        refresh();
172    }
173
174    /**
175     * Populates the model with the relation members represented as a collection of
176     * {@link RelationToChildReference}s.
177     *
178     * @param references the references. Empty list assumed if null.
179     */
180    public void populate(Collection<RelationToChildReference> references) {
181        references = references == null ? new LinkedList<>() : references;
182        decisions.clear();
183        this.relations = new HashSet<>(references.size());
184        final Collection<OsmPrimitive> primitives = new HashSet<>();
185        for (RelationToChildReference reference: references) {
186            decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition()));
187            relations.add(reference.getParent());
188            primitives.add(reference.getChild());
189        }
190        this.primitives = primitives;
191        refresh();
192    }
193
194    /**
195     * Prepare the default decisions for the current model.
196     *
197     * Keep/delete decisions are made if every member has the same role and the members are in consecutive order within the relation.
198     * For multiple occurrences those conditions are tested stepwise for each occurrence.
199     */
200    public void prepareDefaultRelationDecisions() {
201
202        if (primitives.stream().allMatch(Node.class::isInstance)) {
203            final Collection<OsmPrimitive> primitivesInDecisions = new HashSet<>();
204            for (final RelationMemberConflictDecision i : decisions) {
205                primitivesInDecisions.add(i.getOriginalPrimitive());
206            }
207            if (primitivesInDecisions.size() == 1) {
208                for (final RelationMemberConflictDecision i : decisions) {
209                    i.decide(RelationMemberConflictDecisionType.KEEP);
210                }
211                refresh();
212                return;
213            }
214        }
215
216        for (final Relation relation : relations) {
217            final Map<OsmPrimitive, List<RelationMemberConflictDecision>> decisionsByPrimitive = new LinkedHashMap<>(primitives.size(), 1);
218            for (final RelationMemberConflictDecision decision : decisions) {
219                if (decision.getRelation() == relation) {
220                    final OsmPrimitive primitive = decision.getOriginalPrimitive();
221                    if (!decisionsByPrimitive.containsKey(primitive)) {
222                        decisionsByPrimitive.put(primitive, new ArrayList<RelationMemberConflictDecision>());
223                    }
224                    decisionsByPrimitive.get(primitive).add(decision);
225                }
226            }
227
228            //noinspection StatementWithEmptyBody
229            if (!decisionsByPrimitive.keySet().containsAll(primitives)) {
230                // some primitives are not part of the relation, leave undecided
231            } else {
232                final Collection<Iterator<RelationMemberConflictDecision>> iterators = new ArrayList<>(primitives.size());
233                for (final Collection<RelationMemberConflictDecision> i : decisionsByPrimitive.values()) {
234                    iterators.add(i.iterator());
235                }
236                while (iterators.stream().allMatch(Iterator::hasNext)) {
237                    final List<RelationMemberConflictDecision> decisions = new ArrayList<>();
238                    final Collection<String> roles = new HashSet<>();
239                    final Collection<Integer> indices = new TreeSet<>();
240                    for (Iterator<RelationMemberConflictDecision> it : iterators) {
241                        final RelationMemberConflictDecision decision = it.next();
242                        decisions.add(decision);
243                        roles.add(decision.getRole());
244                        indices.add(decision.getPos());
245                    }
246                    if (roles.size() != 1) {
247                        // roles to not patch, leave undecided
248                        continue;
249                    } else if (!isCollectionOfConsecutiveNumbers(indices)) {
250                        // not consecutive members in relation, leave undecided
251                        continue;
252                    }
253                    decisions.get(0).decide(RelationMemberConflictDecisionType.KEEP);
254                    for (RelationMemberConflictDecision decision : decisions.subList(1, decisions.size())) {
255                        decision.decide(RelationMemberConflictDecisionType.REMOVE);
256                    }
257                }
258            }
259        }
260
261        refresh();
262    }
263
264    static boolean isCollectionOfConsecutiveNumbers(Collection<Integer> numbers) {
265        if (numbers.isEmpty()) {
266            return true;
267        }
268        final Iterator<Integer> it = numbers.iterator();
269        Integer previousValue = it.next();
270        while (it.hasNext()) {
271            final Integer i = it.next();
272            if (previousValue + 1 != i) {
273                return false;
274            }
275            previousValue = i;
276        }
277        return true;
278    }
279
280    /**
281     * Replies the decision at position <code>row</code>
282     *
283     * @param row position
284     * @return the decision at position <code>row</code>
285     */
286    public RelationMemberConflictDecision getDecision(int row) {
287        return decisions.get(row);
288    }
289
290    /**
291     * Replies the number of decisions managed by this model
292     *
293     * @return the number of decisions managed by this model
294     */
295    public int getNumDecisions() {
296        return decisions == null /* accessed via super constructor */ ? 0 : decisions.size();
297    }
298
299    /**
300     * Refreshes the model state. Invoke this method to trigger necessary change
301     * events after an update of the model data.
302     *
303     */
304    public void refresh() {
305        updateNumConflicts();
306        GuiHelper.runInEDTAndWait(this::fireTableDataChanged);
307    }
308
309    /**
310     * Apply a role to all member managed by this model.
311     *
312     * @param role the role. Empty string assumed if null.
313     */
314    public void applyRole(String role) {
315        role = role == null ? "" : role;
316        for (RelationMemberConflictDecision decision : decisions) {
317            decision.setRole(role);
318        }
319        refresh();
320    }
321
322    protected RelationMemberConflictDecision getDecision(Relation relation, int pos) {
323        for (RelationMemberConflictDecision decision: decisions) {
324            if (decision.matches(relation, pos)) return decision;
325        }
326        return null;
327    }
328
329    protected Command buildResolveCommand(Relation relation, OsmPrimitive newPrimitive) {
330        final Relation modifiedRelation = new Relation(relation);
331        modifiedRelation.setMembers(null);
332        boolean isChanged = false;
333        for (int i = 0; i < relation.getMembersCount(); i++) {
334            final RelationMember member = relation.getMember(i);
335            RelationMemberConflictDecision decision = getDecision(relation, i);
336            if (decision == null) {
337                modifiedRelation.addMember(member);
338            } else {
339                switch(decision.getDecision()) {
340                case KEEP:
341                    final RelationMember newMember = new RelationMember(decision.getRole(), newPrimitive);
342                    modifiedRelation.addMember(newMember);
343                    isChanged |= !member.equals(newMember);
344                    break;
345                case REMOVE:
346                    isChanged = true;
347                    // do nothing
348                    break;
349                case UNDECIDED:
350                    // FIXME: this is an error
351                    break;
352                }
353            }
354        }
355        if (isChanged)
356            return new ChangeCommand(relation, modifiedRelation);
357        return null;
358    }
359
360    /**
361     * Builds a collection of commands executing the decisions made in this model.
362     *
363     * @param newPrimitive the primitive which members shall refer to
364     * @return a list of commands
365     */
366    public List<Command> buildResolutionCommands(OsmPrimitive newPrimitive) {
367        List<Command> command = new LinkedList<>();
368        for (Relation relation : relations) {
369            Command cmd = buildResolveCommand(relation, newPrimitive);
370            if (cmd != null) {
371                command.add(cmd);
372            }
373        }
374        return command;
375    }
376
377    protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) {
378        for (int i = 0; i < relation.getMembersCount(); i++) {
379            RelationMemberConflictDecision decision = getDecision(relation, i);
380            if (decision == null) {
381                continue;
382            }
383            switch(decision.getDecision()) {
384            case REMOVE: return true;
385            case KEEP:
386                if (!relation.getMember(i).getRole().equals(decision.getRole()))
387                    return true;
388                if (relation.getMember(i).getMember() != newPrimitive)
389                    return true;
390            case UNDECIDED:
391                // FIXME: handle error
392            }
393        }
394        return false;
395    }
396
397    /**
398     * Replies the set of relations which have to be modified according
399     * to the decisions managed by this model.
400     *
401     * @param newPrimitive the primitive which members shall refer to
402     *
403     * @return the set of relations which have to be modified according
404     * to the decisions managed by this model
405     */
406    public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) {
407        Set<Relation> ret = new HashSet<>();
408        for (Relation relation: relations) {
409            if (isChanged(relation, newPrimitive)) {
410                ret.add(relation);
411            }
412        }
413        return ret;
414    }
415}