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