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