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.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.RelationMember;
025import org.openstreetmap.josm.data.osm.RelationToChildReference;
026import org.openstreetmap.josm.gui.util.GuiHelper;
027import org.openstreetmap.josm.tools.Predicate;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * This model manages a list of conflicting relation members.
032 *
033 * It can be used as {@link javax.swing.table.TableModel}.
034 */
035public class RelationMemberConflictResolverModel extends DefaultTableModel {
036    /** the property name for the number conflicts managed by this model */
037    public static final String NUM_CONFLICTS_PROP = RelationMemberConflictResolverModel.class.getName() + ".numConflicts";
038
039    /** the list of conflict decisions */
040    protected final transient List<RelationMemberConflictDecision> decisions;
041    /** the collection of relations for which we manage conflicts */
042    protected transient Collection<Relation> relations;
043    /** the collection of primitives for which we manage conflicts */
044    protected transient Collection<? extends OsmPrimitive> primitives;
045    /** the number of conflicts */
046    private int numConflicts;
047    private final PropertyChangeSupport support;
048
049    /**
050     * Replies true if each {@link MultiValueResolutionDecision} is decided.
051     *
052     * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise
053     */
054    public boolean isResolvedCompletely() {
055        return numConflicts == 0;
056    }
057
058    /**
059     * Replies the current number of conflicts
060     *
061     * @return the current number of conflicts
062     */
063    public int getNumConflicts() {
064        return numConflicts;
065    }
066
067    /**
068     * Updates the current number of conflicts from list of decisions and emits
069     * a property change event if necessary.
070     *
071     */
072    protected void updateNumConflicts() {
073        int count = 0;
074        for (RelationMemberConflictDecision decision: decisions) {
075            if (!decision.isDecided()) {
076                count++;
077            }
078        }
079        int oldValue = numConflicts;
080        numConflicts = count;
081        if (numConflicts != oldValue) {
082            support.firePropertyChange(getProperty(), oldValue, numConflicts);
083        }
084    }
085
086    protected String getProperty() {
087        return NUM_CONFLICTS_PROP;
088    }
089
090    public void addPropertyChangeListener(PropertyChangeListener l) {
091        support.addPropertyChangeListener(l);
092    }
093
094    public void removePropertyChangeListener(PropertyChangeListener l) {
095        support.removePropertyChangeListener(l);
096    }
097
098    public RelationMemberConflictResolverModel() {
099        decisions = new ArrayList<>();
100        support = new PropertyChangeSupport(this);
101    }
102
103    @Override
104    public int getRowCount() {
105        return getNumDecisions();
106    }
107
108    @Override
109    public Object getValueAt(int row, int column) {
110        if (decisions == null) return null;
111
112        RelationMemberConflictDecision d = decisions.get(row);
113        switch(column) {
114        case 0: /* relation */ return d.getRelation();
115        case 1: /* pos */ return Integer.toString(d.getPos() + 1); // position in "user space" starting at 1
116        case 2: /* role */ return d.getRole();
117        case 3: /* original */ return d.getOriginalPrimitive();
118        case 4: /* decision */ return 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 */
131            d.decide((RelationMemberConflictDecisionType) value);
132            refresh();
133            break;
134        default: // Do nothing
135        }
136        fireTableDataChanged();
137    }
138
139    /**
140     * Populates the model with the members of the relation <code>relation</code>
141     * referring to <code>primitive</code>.
142     *
143     * @param relation the parent relation
144     * @param primitive the child primitive
145     */
146    protected void populate(Relation relation, OsmPrimitive primitive) {
147        for (int i = 0; i < relation.getMembersCount(); i++) {
148            if (relation.getMember(i).refersTo(primitive)) {
149                decisions.add(new RelationMemberConflictDecision(relation, i));
150            }
151        }
152    }
153
154    /**
155     * Populates the model with the relation members belonging to one of the relations in <code>relations</code>
156     * and referring to one of the primitives in <code>memberPrimitives</code>.
157     *
158     * @param relations  the parent relations. Empty list assumed if null.
159     * @param memberPrimitives the child primitives. Empty list assumed if null.
160     */
161    public void populate(Collection<Relation> relations, Collection<? extends OsmPrimitive> memberPrimitives) {
162        decisions.clear();
163        relations = relations == null ? Collections.<Relation>emptyList() : relations;
164        memberPrimitives = memberPrimitives == null ? new LinkedList<OsmPrimitive>() : memberPrimitives;
165        for (Relation r : relations) {
166            for (OsmPrimitive p: memberPrimitives) {
167                populate(r, p);
168            }
169        }
170        this.relations = relations;
171        this.primitives = memberPrimitives;
172        refresh();
173    }
174
175    /**
176     * Populates the model with the relation members represented as a collection of
177     * {@link RelationToChildReference}s.
178     *
179     * @param references the references. Empty list assumed if null.
180     */
181    public void populate(Collection<RelationToChildReference> references) {
182        references = references == null ? new LinkedList<RelationToChildReference>() : references;
183        decisions.clear();
184        this.relations = new HashSet<>(references.size());
185        final Collection<OsmPrimitive> primitives = new HashSet<>();
186        for (RelationToChildReference reference: references) {
187            decisions.add(new RelationMemberConflictDecision(reference.getParent(), reference.getPosition()));
188            relations.add(reference.getParent());
189            primitives.add(reference.getChild());
190        }
191        this.primitives = primitives;
192        refresh();
193    }
194
195    /**
196     * Prepare the default decisions for the current model.
197     *
198     * Keep/delete decisions are made if every member has the same role and the members are in consecutive order within the relation.
199     * For multiple occurrences those conditions are tested stepwise for each occurrence.
200     */
201    public void prepareDefaultRelationDecisions() {
202
203        if (Utils.forAll(primitives, OsmPrimitive.nodePredicate)) {
204            final Collection<OsmPrimitive> primitivesInDecisions = new HashSet<>();
205            for (final RelationMemberConflictDecision i : decisions) {
206                primitivesInDecisions.add(i.getOriginalPrimitive());
207            }
208            if (primitivesInDecisions.size() == 1) {
209                for (final RelationMemberConflictDecision i : decisions) {
210                    i.decide(RelationMemberConflictDecisionType.KEEP);
211                }
212                refresh();
213                return;
214            }
215        }
216
217        for (final Relation relation : relations) {
218            final Map<OsmPrimitive, List<RelationMemberConflictDecision>> decisionsByPrimitive = new LinkedHashMap<>(primitives.size(), 1);
219            for (final RelationMemberConflictDecision decision : decisions) {
220                if (decision.getRelation() == relation) {
221                    final OsmPrimitive primitive = decision.getOriginalPrimitive();
222                    if (!decisionsByPrimitive.containsKey(primitive)) {
223                        decisionsByPrimitive.put(primitive, new ArrayList<RelationMemberConflictDecision>());
224                    }
225                    decisionsByPrimitive.get(primitive).add(decision);
226                }
227            }
228
229            //noinspection StatementWithEmptyBody
230            if (!decisionsByPrimitive.keySet().containsAll(primitives)) {
231                // some primitives are not part of the relation, leave undecided
232            } else {
233                final Collection<Iterator<RelationMemberConflictDecision>> iterators = new ArrayList<>(primitives.size());
234                for (final Collection<RelationMemberConflictDecision> i : decisionsByPrimitive.values()) {
235                    iterators.add(i.iterator());
236                }
237                while (Utils.forAll(iterators, new Predicate<Iterator<RelationMemberConflictDecision>>() {
238                    @Override
239                    public boolean evaluate(Iterator<RelationMemberConflictDecision> it) {
240                        return it.hasNext();
241                    }
242                })) {
243                    final List<RelationMemberConflictDecision> decisions = new ArrayList<>();
244                    final Collection<String> roles = new HashSet<>();
245                    final Collection<Integer> indices = new TreeSet<>();
246                    for (Iterator<RelationMemberConflictDecision> it : iterators) {
247                        final RelationMemberConflictDecision decision = it.next();
248                        decisions.add(decision);
249                        roles.add(decision.getRole());
250                        indices.add(decision.getPos());
251                    }
252                    if (roles.size() != 1) {
253                        // roles to not patch, leave undecided
254                        continue;
255                    } else if (!isCollectionOfConsecutiveNumbers(indices)) {
256                        // not consecutive members in relation, leave undecided
257                        continue;
258                    }
259                    decisions.get(0).decide(RelationMemberConflictDecisionType.KEEP);
260                    for (RelationMemberConflictDecision decision : decisions.subList(1, decisions.size())) {
261                        decision.decide(RelationMemberConflictDecisionType.REMOVE);
262                    }
263                }
264            }
265        }
266
267        refresh();
268    }
269
270    static boolean isCollectionOfConsecutiveNumbers(Collection<Integer> numbers) {
271        if (numbers.isEmpty()) {
272            return true;
273        }
274        final Iterator<Integer> it = numbers.iterator();
275        Integer previousValue = it.next();
276        while (it.hasNext()) {
277            final Integer i = it.next();
278            if (previousValue + 1 != i) {
279                return false;
280            }
281            previousValue = i;
282        }
283        return true;
284    }
285
286    /**
287     * Replies the decision at position <code>row</code>
288     *
289     * @param row position
290     * @return the decision at position <code>row</code>
291     */
292    public RelationMemberConflictDecision getDecision(int row) {
293        return decisions.get(row);
294    }
295
296    /**
297     * Replies the number of decisions managed by this model
298     *
299     * @return the number of decisions managed by this model
300     */
301    public int getNumDecisions() {
302        return decisions == null /* accessed via super constructor */ ? 0 : decisions.size();
303    }
304
305    /**
306     * Refreshes the model state. Invoke this method to trigger necessary change
307     * events after an update of the model data.
308     *
309     */
310    public void refresh() {
311        updateNumConflicts();
312        GuiHelper.runInEDTAndWait(new Runnable() {
313            @Override public void run() {
314                fireTableDataChanged();
315            }
316        });
317    }
318
319    /**
320     * Apply a role to all member managed by this model.
321     *
322     * @param role the role. Empty string assumed if null.
323     */
324    public void applyRole(String role) {
325        role = role == null ? "" : role;
326        for (RelationMemberConflictDecision decision : decisions) {
327            decision.setRole(role);
328        }
329        refresh();
330    }
331
332    protected RelationMemberConflictDecision getDecision(Relation relation, int pos) {
333        for (RelationMemberConflictDecision decision: decisions) {
334            if (decision.matches(relation, pos)) return decision;
335        }
336        return null;
337    }
338
339    protected Command buildResolveCommand(Relation relation, OsmPrimitive newPrimitive) {
340        final Relation modifiedRelation = new Relation(relation);
341        modifiedRelation.setMembers(null);
342        boolean isChanged = false;
343        for (int i = 0; i < relation.getMembersCount(); i++) {
344            final RelationMember member = relation.getMember(i);
345            RelationMemberConflictDecision decision = getDecision(relation, i);
346            if (decision == null) {
347                modifiedRelation.addMember(member);
348            } else {
349                switch(decision.getDecision()) {
350                case KEEP:
351                    final RelationMember newMember = new RelationMember(decision.getRole(), newPrimitive);
352                    modifiedRelation.addMember(newMember);
353                    isChanged |= !member.equals(newMember);
354                    break;
355                case REMOVE:
356                    isChanged = true;
357                    // do nothing
358                    break;
359                case UNDECIDED:
360                    // FIXME: this is an error
361                    break;
362                }
363            }
364        }
365        if (isChanged)
366            return new ChangeCommand(relation, modifiedRelation);
367        return null;
368    }
369
370    /**
371     * Builds a collection of commands executing the decisions made in this model.
372     *
373     * @param newPrimitive the primitive which members shall refer to
374     * @return a list of commands
375     */
376    public List<Command> buildResolutionCommands(OsmPrimitive newPrimitive) {
377        List<Command> command = new LinkedList<>();
378        for (Relation relation : relations) {
379            Command cmd = buildResolveCommand(relation, newPrimitive);
380            if (cmd != null) {
381                command.add(cmd);
382            }
383        }
384        return command;
385    }
386
387    protected boolean isChanged(Relation relation, OsmPrimitive newPrimitive) {
388        for (int i = 0; i < relation.getMembersCount(); i++) {
389            RelationMemberConflictDecision decision = getDecision(relation, i);
390            if (decision == null) {
391                continue;
392            }
393            switch(decision.getDecision()) {
394            case REMOVE: return true;
395            case KEEP:
396                if (!relation.getMember(i).getRole().equals(decision.getRole()))
397                    return true;
398                if (relation.getMember(i).getMember() != newPrimitive)
399                    return true;
400            case UNDECIDED:
401                // FIXME: handle error
402            }
403        }
404        return false;
405    }
406
407    /**
408     * Replies the set of relations which have to be modified according
409     * to the decisions managed by this model.
410     *
411     * @param newPrimitive the primitive which members shall refer to
412     *
413     * @return the set of relations which have to be modified according
414     * to the decisions managed by this model
415     */
416    public Set<Relation> getModifiedRelations(OsmPrimitive newPrimitive) {
417        Set<Relation> ret = new HashSet<>();
418        for (Relation relation: relations) {
419            if (isChanged(relation, newPrimitive)) {
420                ret.add(relation);
421            }
422        }
423        return ret;
424    }
425}