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    static public 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 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(conflicts.size());
206            }
207        });
208    }
209
210    private void updateTitle(int conflictsCount) {
211        if (conflictsCount > 0) {
212            setTitle(tr("Conflicts: {0} unresolved", conflicts.size()));
213        } else {
214            setTitle(tr("Conflict"));
215        }
216    }
217
218    /**
219     * Paints all conflicts that can be expressed on the main window.
220     *
221     * @param g The {@code Graphics} used to paint
222     * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes
223     * @since 86
224     */
225    public void paintConflicts(final Graphics g, final NavigatableComponent nc) {
226        Color preferencesColor = getColor();
227        if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black)))
228            return;
229        g.setColor(preferencesColor);
230        Visitor conflictPainter = new AbstractVisitor() {
231            // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938)
232            private final Set<Relation> visited = new HashSet<Relation>();
233            @Override
234            public void visit(Node n) {
235                Point p = nc.getPoint(n);
236                g.drawRect(p.x-1, p.y-1, 2, 2);
237            }
238            public void visit(Node n1, Node n2) {
239                Point p1 = nc.getPoint(n1);
240                Point p2 = nc.getPoint(n2);
241                g.drawLine(p1.x, p1.y, p2.x, p2.y);
242            }
243            @Override
244            public void visit(Way w) {
245                Node lastN = null;
246                for (Node n : w.getNodes()) {
247                    if (lastN == null) {
248                        lastN = n;
249                        continue;
250                    }
251                    visit(lastN, n);
252                    lastN = n;
253                }
254            }
255            @Override
256            public void visit(Relation e) {
257                if (!visited.contains(e)) {
258                    visited.add(e);
259                    try {
260                        for (RelationMember em : e.getMembers()) {
261                            em.getMember().accept(this);
262                        }
263                    } finally {
264                        visited.remove(e);
265                    }
266                }
267            }
268        };
269        for (Object o : lstConflicts.getSelectedValues()) {
270            if (conflicts == null || !conflicts.hasConflictForMy((OsmPrimitive)o)) {
271                continue;
272            }
273            conflicts.getConflictForMy((OsmPrimitive)o).getTheir().accept(conflictPainter);
274        }
275    }
276
277    @Override
278    public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
279        if (oldLayer != null) {
280            oldLayer.getConflicts().removeConflictListener(this);
281        }
282        if (newLayer != null) {
283            newLayer.getConflicts().addConflictListener(this);
284        }
285        refreshView();
286    }
287
288
289    /**
290     * replies the conflict collection currently held by this dialog; may be null
291     *
292     * @return the conflict collection currently held by this dialog; may be null
293     */
294    public ConflictCollection getConflicts() {
295        return conflicts;
296    }
297
298    /**
299     * returns the first selected item of the conflicts list
300     *
301     * @return Conflict
302     */
303    public Conflict<? extends OsmPrimitive> getSelectedConflict() {
304        if (conflicts == null || model.getSize() == 0) return null;
305
306        int index = lstConflicts.getSelectedIndex();
307        if (index < 0) return null;
308
309        return conflicts.get(index);
310    }
311
312    @Override
313    public void onConflictsAdded(ConflictCollection conflicts) {
314        refreshView();
315    }
316
317    @Override
318    public void onConflictsRemoved(ConflictCollection conflicts) {
319        Main.info("1 conflict has been resolved.");
320        refreshView();
321    }
322
323    @Override
324    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
325        lstConflicts.clearSelection();
326        for (OsmPrimitive osm : newSelection) {
327            if (conflicts != null && conflicts.hasConflictForMy(osm)) {
328                int pos = model.indexOf(osm);
329                if (pos >= 0) {
330                    lstConflicts.addSelectionInterval(pos, pos);
331                }
332            }
333        }
334    }
335
336    @Override
337    public String helpTopic() {
338        return ht("/Dialog/ConflictList");
339    }
340
341    class MouseEventHandler extends PopupMenuLauncher {
342        public MouseEventHandler() {
343            super(popupMenu);
344        }
345        @Override public void mouseClicked(MouseEvent e) {
346            if (isDoubleClick(e)) {
347                resolve();
348            }
349        }
350    }
351
352    /**
353     * The {@link ListModel} for conflicts
354     *
355     */
356    class ConflictListModel implements ListModel {
357
358        private CopyOnWriteArrayList<ListDataListener> listeners;
359
360        public ConflictListModel() {
361            listeners = new CopyOnWriteArrayList<ListDataListener>();
362        }
363
364        @Override
365        public void addListDataListener(ListDataListener l) {
366            if (l != null) {
367                listeners.addIfAbsent(l);
368            }
369        }
370
371        @Override
372        public void removeListDataListener(ListDataListener l) {
373            listeners.remove(l);
374        }
375
376        protected void fireContentChanged() {
377            ListDataEvent evt = new ListDataEvent(
378                    this,
379                    ListDataEvent.CONTENTS_CHANGED,
380                    0,
381                    getSize()
382            );
383            for (ListDataListener listener : listeners) {
384                listener.contentsChanged(evt);
385            }
386        }
387
388        @Override
389        public Object getElementAt(int index) {
390            if (index < 0) return null;
391            if (index >= getSize()) return null;
392            return conflicts.get(index).getMy();
393        }
394
395        @Override
396        public int getSize() {
397            if (conflicts == null) return 0;
398            return conflicts.size();
399        }
400
401        public int indexOf(OsmPrimitive my) {
402            if (conflicts == null) return -1;
403            for (int i=0; i < conflicts.size();i++) {
404                if (conflicts.get(i).isMatchingMy(my))
405                    return i;
406            }
407            return -1;
408        }
409
410        public OsmPrimitive get(int idx) {
411            if (conflicts == null) return null;
412            return conflicts.get(idx).getMy();
413        }
414    }
415
416    class ResolveAction extends AbstractAction implements ListSelectionListener {
417        public ResolveAction() {
418            putValue(NAME, tr("Resolve"));
419            putValue(SHORT_DESCRIPTION,  tr("Open a merge dialog of all selected items in the list above."));
420            putValue(SMALL_ICON, ImageProvider.get("dialogs", "conflict"));
421            putValue("help", ht("/Dialog/ConflictList#ResolveAction"));
422        }
423
424        @Override
425        public void actionPerformed(ActionEvent e) {
426            resolve();
427        }
428
429        @Override
430        public void valueChanged(ListSelectionEvent e) {
431            ListSelectionModel model = (ListSelectionModel)e.getSource();
432            boolean enabled = model.getMinSelectionIndex() >= 0
433            && model.getMaxSelectionIndex() >= model.getMinSelectionIndex();
434            setEnabled(enabled);
435        }
436    }
437
438    class SelectAction extends AbstractAction implements ListSelectionListener {
439        public SelectAction() {
440            putValue(NAME, tr("Select"));
441            putValue(SHORT_DESCRIPTION,  tr("Set the selected elements on the map to the selected items in the list above."));
442            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
443            putValue("help", ht("/Dialog/ConflictList#SelectAction"));
444        }
445
446        @Override
447        public void actionPerformed(ActionEvent e) {
448            Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>();
449            for (Object o : lstConflicts.getSelectedValues()) {
450                sel.add((OsmPrimitive)o);
451            }
452            DataSet ds = Main.main.getCurrentDataSet();
453            if (ds != null) { // Can't see how it is possible but it happened in #7942
454                ds.setSelected(sel);
455            }
456        }
457
458        @Override
459        public void valueChanged(ListSelectionEvent e) {
460            ListSelectionModel model = (ListSelectionModel)e.getSource();
461            boolean enabled = model.getMinSelectionIndex() >= 0
462            && model.getMaxSelectionIndex() >= model.getMinSelectionIndex();
463            setEnabled(enabled);
464        }
465    }
466
467    /**
468     * Warns the user about the number of detected conflicts
469     *
470     * @param numNewConflicts the number of detected conflicts
471     * @since 5775
472     */
473    public void warnNumNewConflicts(int numNewConflicts) {
474        if (numNewConflicts == 0) return;
475
476        String msg1 = trn(
477                "There was {0} conflict detected.",
478                "There were {0} conflicts detected.",
479                numNewConflicts,
480                numNewConflicts
481        );
482
483        final StringBuffer sb = new StringBuffer();
484        sb.append("<html>").append(msg1).append("</html>");
485        if (numNewConflicts > 0) {
486            final ButtonSpec[] options = new ButtonSpec[] {
487                    new ButtonSpec(
488                            tr("OK"),
489                            ImageProvider.get("ok"),
490                            tr("Click to close this dialog and continue editing"),
491                            null /* no specific help */
492                    )
493            };
494            GuiHelper.runInEDT(new Runnable() {
495                @Override
496                public void run() {
497                    HelpAwareOptionPane.showOptionDialog(
498                            Main.parent,
499                            sb.toString(),
500                            tr("Conflicts detected"),
501                            JOptionPane.WARNING_MESSAGE,
502                            null, /* no icon */
503                            options,
504                            options[0],
505                            ht("/Concepts/Conflict#WarningAboutDetectedConflicts")
506                    );
507                    unfurlDialog();
508                    Main.map.repaint();
509                }
510            });
511        }
512    }
513}