001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.awt.Color;
010import java.awt.Graphics;
011import java.awt.Point;
012import java.awt.event.ActionEvent;
013import java.awt.event.KeyEvent;
014import java.awt.event.MouseEvent;
015import java.util.ArrayList;
016import java.util.Arrays;
017import java.util.Collection;
018import java.util.HashSet;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Set;
022import java.util.concurrent.CopyOnWriteArrayList;
023
024import javax.swing.AbstractAction;
025import javax.swing.JList;
026import javax.swing.JMenuItem;
027import javax.swing.JOptionPane;
028import javax.swing.JPopupMenu;
029import javax.swing.ListModel;
030import javax.swing.ListSelectionModel;
031import javax.swing.event.ListDataEvent;
032import javax.swing.event.ListDataListener;
033import javax.swing.event.ListSelectionEvent;
034import javax.swing.event.ListSelectionListener;
035import javax.swing.event.PopupMenuEvent;
036import javax.swing.event.PopupMenuListener;
037
038import org.openstreetmap.josm.Main;
039import org.openstreetmap.josm.actions.AbstractSelectAction;
040import org.openstreetmap.josm.actions.ExpertToggleAction;
041import org.openstreetmap.josm.command.Command;
042import org.openstreetmap.josm.command.SequenceCommand;
043import org.openstreetmap.josm.data.SelectionChangedListener;
044import org.openstreetmap.josm.data.conflict.Conflict;
045import org.openstreetmap.josm.data.conflict.ConflictCollection;
046import org.openstreetmap.josm.data.conflict.IConflictListener;
047import org.openstreetmap.josm.data.osm.DataSet;
048import org.openstreetmap.josm.data.osm.Node;
049import org.openstreetmap.josm.data.osm.OsmPrimitive;
050import org.openstreetmap.josm.data.osm.Relation;
051import org.openstreetmap.josm.data.osm.RelationMember;
052import org.openstreetmap.josm.data.osm.Way;
053import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
054import org.openstreetmap.josm.data.osm.visitor.Visitor;
055import org.openstreetmap.josm.gui.HelpAwareOptionPane;
056import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
057import org.openstreetmap.josm.gui.NavigatableComponent;
058import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
059import org.openstreetmap.josm.gui.PopupMenuHandler;
060import org.openstreetmap.josm.gui.SideButton;
061import org.openstreetmap.josm.gui.conflict.pair.ConflictResolver;
062import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
063import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
064import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
065import org.openstreetmap.josm.gui.layer.OsmDataLayer;
066import org.openstreetmap.josm.gui.util.GuiHelper;
067import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
068import org.openstreetmap.josm.tools.ImageProvider;
069import org.openstreetmap.josm.tools.Shortcut;
070
071/**
072 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle
073 * dialog on the right of the main frame.
074 * @since 86
075 */
076public final class ConflictDialog extends ToggleDialog implements ActiveLayerChangeListener, IConflictListener, SelectionChangedListener {
077
078    /** the collection of conflicts displayed by this conflict dialog */
079    private transient ConflictCollection conflicts;
080
081    /** the model for the list of conflicts */
082    private transient ConflictListModel model;
083    /** the list widget for the list of conflicts */
084    private JList<OsmPrimitive> lstConflicts;
085
086    private final JPopupMenu popupMenu = new JPopupMenu();
087    private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
088
089    private final ResolveAction actResolve = new ResolveAction();
090    private final SelectAction actSelect = new SelectAction();
091
092    /**
093     * Constructs a new {@code ConflictDialog}.
094     */
095    public ConflictDialog() {
096        super(tr("Conflict"), "conflict", tr("Resolve conflicts."),
097                Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")),
098                KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100);
099
100        build();
101        refreshView();
102    }
103
104    /**
105     * Replies the color used to paint conflicts.
106     *
107     * @return the color used to paint conflicts
108     * @see #paintConflicts
109     * @since 1221
110     */
111    public static Color getColor() {
112        return Main.pref.getColor(marktr("conflict"), Color.gray);
113    }
114
115    /**
116     * builds the GUI
117     */
118    protected void build() {
119        model = new ConflictListModel();
120
121        lstConflicts = new JList<>(model);
122        lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
123        lstConflicts.setCellRenderer(new OsmPrimitivRenderer());
124        lstConflicts.addMouseListener(new MouseEventHandler());
125        addListSelectionListener(new ListSelectionListener() {
126            @Override
127            public void valueChanged(ListSelectionEvent e) {
128                Main.map.mapView.repaint();
129            }
130        });
131
132        SideButton btnResolve = new SideButton(actResolve);
133        addListSelectionListener(actResolve);
134
135        SideButton btnSelect = new SideButton(actSelect);
136        addListSelectionListener(actSelect);
137
138        createLayout(lstConflicts, true, Arrays.asList(new SideButton[] {
139            btnResolve, btnSelect
140        }));
141
142        popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("conflict"));
143
144        final ResolveToMyVersionAction resolveToMyVersionAction = new ResolveToMyVersionAction();
145        final ResolveToTheirVersionAction resolveToTheirVersionAction = new ResolveToTheirVersionAction();
146        addListSelectionListener(resolveToMyVersionAction);
147        addListSelectionListener(resolveToTheirVersionAction);
148        final JMenuItem btnResolveMy = popupMenuHandler.addAction(resolveToMyVersionAction);
149        final JMenuItem btnResolveTheir = popupMenuHandler.addAction(resolveToTheirVersionAction);
150
151        popupMenuHandler.addListener(new PopupMenuListener() {
152            @Override
153            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
154                btnResolveMy.setVisible(ExpertToggleAction.isExpert());
155                btnResolveTheir.setVisible(ExpertToggleAction.isExpert());
156            }
157
158            @Override
159            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
160                // Do nothing
161            }
162
163            @Override
164            public void popupMenuCanceled(PopupMenuEvent e) {
165                // Do nothing
166            }
167        });
168    }
169
170    @Override
171    public void showNotify() {
172        DataSet.addSelectionListener(this);
173        Main.getLayerManager().addAndFireActiveLayerChangeListener(this);
174        refreshView();
175    }
176
177    @Override
178    public void hideNotify() {
179        Main.getLayerManager().removeActiveLayerChangeListener(this);
180        DataSet.removeSelectionListener(this);
181    }
182
183    /**
184     * Add a list selection listener to the conflicts list.
185     * @param listener the ListSelectionListener
186     * @since 5958
187     */
188    public void addListSelectionListener(ListSelectionListener listener) {
189        lstConflicts.getSelectionModel().addListSelectionListener(listener);
190    }
191
192    /**
193     * Remove the given list selection listener from the conflicts list.
194     * @param listener the ListSelectionListener
195     * @since 5958
196     */
197    public void removeListSelectionListener(ListSelectionListener listener) {
198        lstConflicts.getSelectionModel().removeListSelectionListener(listener);
199    }
200
201    /**
202     * Replies the popup menu handler.
203     * @return The popup menu handler
204     * @since 5958
205     */
206    public PopupMenuHandler getPopupMenuHandler() {
207        return popupMenuHandler;
208    }
209
210    /**
211     * Launches a conflict resolution dialog for the first selected conflict
212     */
213    private void resolve() {
214        if (conflicts == null || model.getSize() == 0)
215            return;
216
217        int index = lstConflicts.getSelectedIndex();
218        if (index < 0) {
219            index = 0;
220        }
221
222        Conflict<? extends OsmPrimitive> c = conflicts.get(index);
223        ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent);
224        dialog.getConflictResolver().populate(c);
225        dialog.setVisible(true);
226
227        lstConflicts.setSelectedIndex(index);
228
229        Main.map.mapView.repaint();
230    }
231
232    /**
233     * refreshes the view of this dialog
234     */
235    public void refreshView() {
236        OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
237        conflicts = editLayer == null ? new ConflictCollection() : editLayer.getConflicts();
238        GuiHelper.runInEDT(new Runnable() {
239            @Override
240            public void run() {
241                model.fireContentChanged();
242                updateTitle();
243            }
244        });
245    }
246
247    private void updateTitle() {
248        int conflictsCount = conflicts.size();
249        if (conflictsCount > 0) {
250            setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) +
251                    " ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}",
252                            conflicts.getRelationConflicts().size(),
253                            conflicts.getWayConflicts().size(),
254                            conflicts.getNodeConflicts().size())+')');
255        } else {
256            setTitle(tr("Conflict"));
257        }
258    }
259
260    /**
261     * Paints all conflicts that can be expressed on the main window.
262     *
263     * @param g The {@code Graphics} used to paint
264     * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes
265     * @since 86
266     */
267    public void paintConflicts(final Graphics g, final NavigatableComponent nc) {
268        Color preferencesColor = getColor();
269        if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black)))
270            return;
271        g.setColor(preferencesColor);
272        Visitor conflictPainter = new ConflictPainter(nc, g);
273        for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
274            if (conflicts == null || !conflicts.hasConflictForMy(o)) {
275                continue;
276            }
277            conflicts.getConflictForMy(o).getTheir().accept(conflictPainter);
278        }
279    }
280
281    @Override
282    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
283        OsmDataLayer oldLayer = e.getPreviousEditLayer();
284        if (oldLayer != null) {
285            oldLayer.getConflicts().removeConflictListener(this);
286        }
287        OsmDataLayer newLayer = e.getSource().getEditLayer();
288        if (newLayer != null) {
289            newLayer.getConflicts().addConflictListener(this);
290        }
291        refreshView();
292    }
293
294    /**
295     * replies the conflict collection currently held by this dialog; may be null
296     *
297     * @return the conflict collection currently held by this dialog; may be null
298     */
299    public ConflictCollection getConflicts() {
300        return conflicts;
301    }
302
303    /**
304     * returns the first selected item of the conflicts list
305     *
306     * @return Conflict
307     */
308    public Conflict<? extends OsmPrimitive> getSelectedConflict() {
309        if (conflicts == null || model.getSize() == 0)
310            return null;
311
312        int index = lstConflicts.getSelectedIndex();
313
314        return index >= 0 ? conflicts.get(index) : null;
315    }
316
317    private boolean isConflictSelected() {
318        final ListSelectionModel selModel = lstConflicts.getSelectionModel();
319        return selModel.getMinSelectionIndex() >= 0 && selModel.getMaxSelectionIndex() >= selModel.getMinSelectionIndex();
320    }
321
322    @Override
323    public void onConflictsAdded(ConflictCollection conflicts) {
324        refreshView();
325    }
326
327    @Override
328    public void onConflictsRemoved(ConflictCollection conflicts) {
329        Main.info("1 conflict has been resolved.");
330        refreshView();
331    }
332
333    @Override
334    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
335        lstConflicts.clearSelection();
336        for (OsmPrimitive osm : newSelection) {
337            if (conflicts != null && conflicts.hasConflictForMy(osm)) {
338                int pos = model.indexOf(osm);
339                if (pos >= 0) {
340                    lstConflicts.addSelectionInterval(pos, pos);
341                }
342            }
343        }
344    }
345
346    @Override
347    public String helpTopic() {
348        return ht("/Dialog/ConflictList");
349    }
350
351    class MouseEventHandler extends PopupMenuLauncher {
352        /**
353         * Constructs a new {@code MouseEventHandler}.
354         */
355        MouseEventHandler() {
356            super(popupMenu);
357        }
358
359        @Override public void mouseClicked(MouseEvent e) {
360            if (isDoubleClick(e)) {
361                resolve();
362            }
363        }
364    }
365
366    /**
367     * The {@link ListModel} for conflicts
368     *
369     */
370    class ConflictListModel implements ListModel<OsmPrimitive> {
371
372        private final CopyOnWriteArrayList<ListDataListener> listeners;
373
374        /**
375         * Constructs a new {@code ConflictListModel}.
376         */
377        ConflictListModel() {
378            listeners = new CopyOnWriteArrayList<>();
379        }
380
381        @Override
382        public void addListDataListener(ListDataListener l) {
383            if (l != null) {
384                listeners.addIfAbsent(l);
385            }
386        }
387
388        @Override
389        public void removeListDataListener(ListDataListener l) {
390            listeners.remove(l);
391        }
392
393        protected void fireContentChanged() {
394            ListDataEvent evt = new ListDataEvent(
395                    this,
396                    ListDataEvent.CONTENTS_CHANGED,
397                    0,
398                    getSize()
399            );
400            for (ListDataListener listener : listeners) {
401                listener.contentsChanged(evt);
402            }
403        }
404
405        @Override
406        public OsmPrimitive getElementAt(int index) {
407            if (index < 0 || index >= getSize())
408                return null;
409            return conflicts.get(index).getMy();
410        }
411
412        @Override
413        public int getSize() {
414            return conflicts != null ? conflicts.size() : 0;
415        }
416
417        public int indexOf(OsmPrimitive my) {
418            if (conflicts != null) {
419                for (int i = 0; i < conflicts.size(); i++) {
420                    if (conflicts.get(i).isMatchingMy(my))
421                        return i;
422                }
423            }
424            return -1;
425        }
426
427        public OsmPrimitive get(int idx) {
428            return conflicts != null ? conflicts.get(idx).getMy() : null;
429        }
430    }
431
432    class ResolveAction extends AbstractAction implements ListSelectionListener {
433        ResolveAction() {
434            putValue(NAME, tr("Resolve"));
435            putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above."));
436            new ImageProvider("dialogs", "conflict").getResource().attachImageIcon(this, true);
437            putValue("help", ht("/Dialog/ConflictList#ResolveAction"));
438        }
439
440        @Override
441        public void actionPerformed(ActionEvent e) {
442            resolve();
443        }
444
445        @Override
446        public void valueChanged(ListSelectionEvent e) {
447            setEnabled(isConflictSelected());
448        }
449    }
450
451    final class SelectAction extends AbstractSelectAction implements ListSelectionListener {
452        private SelectAction() {
453            putValue("help", ht("/Dialog/ConflictList#SelectAction"));
454        }
455
456        @Override
457        public void actionPerformed(ActionEvent e) {
458            Collection<OsmPrimitive> sel = new LinkedList<>();
459            for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
460                sel.add(o);
461            }
462            DataSet ds = Main.getLayerManager().getEditDataSet();
463            if (ds != null) { // Can't see how it is possible but it happened in #7942
464                ds.setSelected(sel);
465            }
466        }
467
468        @Override
469        public void valueChanged(ListSelectionEvent e) {
470            setEnabled(isConflictSelected());
471        }
472    }
473
474    abstract class ResolveToAction extends ResolveAction {
475        private final String name;
476        private final MergeDecisionType type;
477
478        ResolveToAction(String name, String description, MergeDecisionType type) {
479            this.name = name;
480            this.type = type;
481            putValue(NAME, name);
482            putValue(SHORT_DESCRIPTION, description);
483        }
484
485        @Override
486        public void actionPerformed(ActionEvent e) {
487            final ConflictResolver resolver = new ConflictResolver();
488            final List<Command> commands = new ArrayList<>();
489            for (OsmPrimitive osmPrimitive : lstConflicts.getSelectedValuesList()) {
490                Conflict<? extends OsmPrimitive> c = conflicts.getConflictForMy(osmPrimitive);
491                if (c != null) {
492                    resolver.populate(c);
493                    resolver.decideRemaining(type);
494                    commands.add(resolver.buildResolveCommand());
495                }
496            }
497            Main.main.undoRedo.add(new SequenceCommand(name, commands));
498            refreshView();
499            Main.map.mapView.repaint();
500        }
501    }
502
503    class ResolveToMyVersionAction extends ResolveToAction {
504        ResolveToMyVersionAction() {
505            super(tr("Resolve to my versions"), tr("Resolves all unresolved conflicts to ''my'' version"),
506                    MergeDecisionType.KEEP_MINE);
507        }
508    }
509
510    class ResolveToTheirVersionAction extends ResolveToAction {
511        ResolveToTheirVersionAction() {
512            super(tr("Resolve to their versions"), tr("Resolves all unresolved conflicts to ''their'' version"),
513                    MergeDecisionType.KEEP_THEIR);
514        }
515    }
516
517    /**
518     * Paints conflicts.
519     */
520    public static class ConflictPainter extends AbstractVisitor {
521        // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938)
522        private final Set<Relation> visited = new HashSet<>();
523        private final NavigatableComponent nc;
524        private final Graphics g;
525
526        ConflictPainter(NavigatableComponent nc, Graphics g) {
527            this.nc = nc;
528            this.g = g;
529        }
530
531        @Override
532        public void visit(Node n) {
533            Point p = nc.getPoint(n);
534            g.drawRect(p.x-1, p.y-1, 2, 2);
535        }
536
537        private void visit(Node n1, Node n2) {
538            Point p1 = nc.getPoint(n1);
539            Point p2 = nc.getPoint(n2);
540            g.drawLine(p1.x, p1.y, p2.x, p2.y);
541        }
542
543        @Override
544        public void visit(Way w) {
545            Node lastN = null;
546            for (Node n : w.getNodes()) {
547                if (lastN == null) {
548                    lastN = n;
549                    continue;
550                }
551                visit(lastN, n);
552                lastN = n;
553            }
554        }
555
556        @Override
557        public void visit(Relation e) {
558            if (!visited.contains(e)) {
559                visited.add(e);
560                try {
561                    for (RelationMember em : e.getMembers()) {
562                        em.getMember().accept(this);
563                    }
564                } finally {
565                    visited.remove(e);
566                }
567            }
568        }
569    }
570
571    /**
572     * Warns the user about the number of detected conflicts
573     *
574     * @param numNewConflicts the number of detected conflicts
575     * @since 5775
576     */
577    public void warnNumNewConflicts(int numNewConflicts) {
578        if (numNewConflicts == 0)
579            return;
580
581        String msg1 = trn(
582                "There was {0} conflict detected.",
583                "There were {0} conflicts detected.",
584                numNewConflicts,
585                numNewConflicts
586        );
587
588        final StringBuilder sb = new StringBuilder();
589        sb.append("<html>").append(msg1).append("</html>");
590        if (numNewConflicts > 0) {
591            final ButtonSpec[] options = new ButtonSpec[] {
592                    new ButtonSpec(
593                            tr("OK"),
594                            ImageProvider.get("ok"),
595                            tr("Click to close this dialog and continue editing"),
596                            null /* no specific help */
597                    )
598            };
599            GuiHelper.runInEDT(new Runnable() {
600                @Override
601                public void run() {
602                    HelpAwareOptionPane.showOptionDialog(
603                            Main.parent,
604                            sb.toString(),
605                            tr("Conflicts detected"),
606                            JOptionPane.WARNING_MESSAGE,
607                            null, /* no icon */
608                            options,
609                            options[0],
610                            ht("/Concepts/Conflict#WarningAboutDetectedConflicts")
611                    );
612                    unfurlDialog();
613                    Main.map.repaint();
614                }
615            });
616        }
617    }
618}