001    // License: GPL. See LICENSE file for details.
002    package org.openstreetmap.josm.gui.dialogs;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.marktr;
006    import static org.openstreetmap.josm.tools.I18n.tr;
007    
008    import java.awt.Color;
009    import java.awt.Graphics;
010    import java.awt.Point;
011    import java.awt.event.ActionEvent;
012    import java.awt.event.KeyEvent;
013    import java.awt.event.MouseAdapter;
014    import java.awt.event.MouseEvent;
015    import java.util.Arrays;
016    import java.util.Collection;
017    import java.util.HashSet;
018    import java.util.Iterator;
019    import java.util.LinkedList;
020    import java.util.Set;
021    import java.util.concurrent.CopyOnWriteArrayList;
022    
023    import javax.swing.AbstractAction;
024    import javax.swing.JList;
025    import javax.swing.ListModel;
026    import javax.swing.ListSelectionModel;
027    import javax.swing.event.ListDataEvent;
028    import javax.swing.event.ListDataListener;
029    import javax.swing.event.ListSelectionEvent;
030    import javax.swing.event.ListSelectionListener;
031    
032    import org.openstreetmap.josm.Main;
033    import org.openstreetmap.josm.data.SelectionChangedListener;
034    import org.openstreetmap.josm.data.conflict.Conflict;
035    import org.openstreetmap.josm.data.conflict.ConflictCollection;
036    import org.openstreetmap.josm.data.conflict.IConflictListener;
037    import org.openstreetmap.josm.data.osm.DataSet;
038    import org.openstreetmap.josm.data.osm.Node;
039    import org.openstreetmap.josm.data.osm.OsmPrimitive;
040    import org.openstreetmap.josm.data.osm.Relation;
041    import org.openstreetmap.josm.data.osm.RelationMember;
042    import org.openstreetmap.josm.data.osm.Way;
043    import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
044    import org.openstreetmap.josm.data.osm.visitor.Visitor;
045    import org.openstreetmap.josm.gui.MapView;
046    import org.openstreetmap.josm.gui.NavigatableComponent;
047    import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
048    import org.openstreetmap.josm.gui.SideButton;
049    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
050    import org.openstreetmap.josm.gui.util.GuiHelper;
051    import org.openstreetmap.josm.tools.ImageProvider;
052    import org.openstreetmap.josm.tools.Shortcut;
053    
054    /**
055     * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle
056     * dialog on the right of the main frame.
057     *
058     */
059    public final class ConflictDialog extends ToggleDialog implements MapView.EditLayerChangeListener, IConflictListener, SelectionChangedListener{
060    
061        /**
062         * Replies the color used to paint conflicts.
063         * 
064         * @return the color used to paint conflicts
065         * @since 1221
066         * @see #paintConflicts
067         */
068        static public Color getColor() {
069            return Main.pref.getColor(marktr("conflict"), Color.gray);
070        }
071    
072        /** the collection of conflicts displayed by this conflict dialog */
073        private ConflictCollection conflicts;
074    
075        /** the model for the list of conflicts */
076        private ConflictListModel model;
077        /** the list widget for the list of conflicts */
078        private JList lstConflicts;
079    
080        private ResolveAction actResolve;
081        private SelectAction actSelect;
082    
083        /**
084         * builds the GUI
085         */
086        protected void build() {
087            model = new ConflictListModel();
088    
089            lstConflicts = new JList(model);
090            lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
091            lstConflicts.setCellRenderer(new OsmPrimitivRenderer());
092            lstConflicts.addMouseListener(new MouseAdapter(){
093                @Override public void mouseClicked(MouseEvent e) {
094                    if (e.getClickCount() >= 2) {
095                        resolve();
096                    }
097                }
098            });
099            lstConflicts.getSelectionModel().addListSelectionListener(new ListSelectionListener(){
100                public void valueChanged(ListSelectionEvent e) {
101                    Main.map.mapView.repaint();
102                }
103            });
104    
105            SideButton btnResolve = new SideButton(actResolve = new ResolveAction());
106            lstConflicts.getSelectionModel().addListSelectionListener(actResolve);
107    
108            SideButton btnSelect = new SideButton(actSelect = new SelectAction());
109            lstConflicts.getSelectionModel().addListSelectionListener(actSelect);
110    
111            createLayout(lstConflicts, true, Arrays.asList(new SideButton[] {
112                btnResolve, btnSelect
113            }));
114        }
115    
116        /**
117         * constructor
118         */
119        public ConflictDialog() {
120            super(tr("Conflict"), "conflict", tr("Resolve conflicts."),
121                    Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")),
122                    KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100);
123    
124            build();
125            refreshView();
126        }
127    
128        @Override
129        public void showNotify() {
130            DataSet.addSelectionListener(this);
131            MapView.addEditLayerChangeListener(this, true);
132            refreshView();
133        }
134    
135        @Override
136        public void hideNotify() {
137            MapView.removeEditLayerChangeListener(this);
138            DataSet.removeSelectionListener(this);
139        }
140    
141        /**
142         * Launches a conflict resolution dialog for the first selected conflict
143         *
144         */
145        private final void resolve() {
146            if (conflicts == null || model.getSize() == 0) return;
147    
148            int index = lstConflicts.getSelectedIndex();
149            if (index < 0) {
150                index = 0;
151            }
152    
153            Conflict<? extends OsmPrimitive> c = conflicts.get(index);
154            ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent);
155            dialog.getConflictResolver().populate(c);
156            dialog.setVisible(true);
157    
158            lstConflicts.setSelectedIndex(index);
159    
160            Main.map.mapView.repaint();
161        }
162    
163        /**
164         * refreshes the view of this dialog
165         */
166        public final void refreshView() {
167            OsmDataLayer editLayer =  Main.main.getEditLayer();
168            conflicts = (editLayer == null ? new ConflictCollection() : editLayer.getConflicts());
169            GuiHelper.runInEDT(new Runnable() {
170                @Override
171                public void run() {
172                    model.fireContentChanged();
173                    updateTitle(conflicts.size());
174                }
175            });
176        }
177    
178        private void updateTitle(int conflictsCount) {
179            if (conflictsCount > 0) {
180                setTitle(tr("Conflicts: {0} unresolved", conflicts.size()));
181            } else {
182                setTitle(tr("Conflict"));
183            }
184        }
185    
186        /**
187         * Paints all conflicts that can be expressed on the main window.
188         * 
189         * @param g The {@code Graphics} used to paint
190         * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes
191         * @since 86
192         */
193        public void paintConflicts(final Graphics g, final NavigatableComponent nc) {
194            Color preferencesColor = getColor();
195            if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black)))
196                return;
197            g.setColor(preferencesColor);
198            Visitor conflictPainter = new AbstractVisitor() {
199                // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938)
200                private final Set<Relation> visited = new HashSet<Relation>();
201                public void visit(Node n) {
202                    Point p = nc.getPoint(n);
203                    g.drawRect(p.x-1, p.y-1, 2, 2);
204                }
205                public void visit(Node n1, Node n2) {
206                    Point p1 = nc.getPoint(n1);
207                    Point p2 = nc.getPoint(n2);
208                    g.drawLine(p1.x, p1.y, p2.x, p2.y);
209                }
210                public void visit(Way w) {
211                    Node lastN = null;
212                    for (Node n : w.getNodes()) {
213                        if (lastN == null) {
214                            lastN = n;
215                            continue;
216                        }
217                        visit(lastN, n);
218                        lastN = n;
219                    }
220                }
221                public void visit(Relation e) {
222                    if (!visited.contains(e)) {
223                        visited.add(e);
224                        try {
225                            for (RelationMember em : e.getMembers()) {
226                                em.getMember().visit(this);
227                            }
228                        } finally {
229                            visited.remove(e);
230                        }
231                    }
232                }
233            };
234            for (Object o : lstConflicts.getSelectedValues()) {
235                if (conflicts == null || !conflicts.hasConflictForMy((OsmPrimitive)o)) {
236                    continue;
237                }
238                conflicts.getConflictForMy((OsmPrimitive)o).getTheir().visit(conflictPainter);
239            }
240        }
241    
242        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
243            if (oldLayer != null) {
244                oldLayer.getConflicts().removeConflictListener(this);
245            }
246            if (newLayer != null) {
247                newLayer.getConflicts().addConflictListener(this);
248            }
249            refreshView();
250        }
251    
252    
253        /**
254         * replies the conflict collection currently held by this dialog; may be null
255         *
256         * @return the conflict collection currently held by this dialog; may be null
257         */
258        public ConflictCollection getConflicts() {
259            return conflicts;
260        }
261    
262        /**
263         * returns the first selected item of the conflicts list
264         * 
265         * @return Conflict
266         */
267        public Conflict<? extends OsmPrimitive> getSelectedConflict() {
268            if (conflicts == null || model.getSize() == 0) return null;
269    
270            int index = lstConflicts.getSelectedIndex();
271            if (index < 0) return null;
272    
273            return conflicts.get(index);
274        }
275    
276        public void onConflictsAdded(ConflictCollection conflicts) {
277            refreshView();
278        }
279    
280        public void onConflictsRemoved(ConflictCollection conflicts) {
281            System.err.println("1 conflict has been resolved.");
282            refreshView();
283        }
284    
285        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
286            lstConflicts.clearSelection();
287            for (OsmPrimitive osm : newSelection) {
288                if (conflicts != null && conflicts.hasConflictForMy(osm)) {
289                    int pos = model.indexOf(osm);
290                    if (pos >= 0) {
291                        lstConflicts.addSelectionInterval(pos, pos);
292                    }
293                }
294            }
295        }
296    
297        @Override
298        public String helpTopic() {
299            return ht("/Dialog/ConflictList");
300        }
301    
302        /**
303         * The {@link ListModel} for conflicts
304         *
305         */
306        class ConflictListModel implements ListModel {
307    
308            private CopyOnWriteArrayList<ListDataListener> listeners;
309    
310            public ConflictListModel() {
311                listeners = new CopyOnWriteArrayList<ListDataListener>();
312            }
313    
314            public void addListDataListener(ListDataListener l) {
315                if (l != null) {
316                    listeners.addIfAbsent(l);
317                }
318            }
319    
320            public void removeListDataListener(ListDataListener l) {
321                listeners.remove(l);
322            }
323    
324            protected void fireContentChanged() {
325                ListDataEvent evt = new ListDataEvent(
326                        this,
327                        ListDataEvent.CONTENTS_CHANGED,
328                        0,
329                        getSize()
330                );
331                Iterator<ListDataListener> it = listeners.iterator();
332                while(it.hasNext()) {
333                    it.next().contentsChanged(evt);
334                }
335            }
336    
337            public Object getElementAt(int index) {
338                if (index < 0) return null;
339                if (index >= getSize()) return null;
340                return conflicts.get(index).getMy();
341            }
342    
343            public int getSize() {
344                if (conflicts == null) return 0;
345                return conflicts.size();
346            }
347    
348            public int indexOf(OsmPrimitive my) {
349                if (conflicts == null) return -1;
350                for (int i=0; i < conflicts.size();i++) {
351                    if (conflicts.get(i).isMatchingMy(my))
352                        return i;
353                }
354                return -1;
355            }
356    
357            public OsmPrimitive get(int idx) {
358                if (conflicts == null) return null;
359                return conflicts.get(idx).getMy();
360            }
361        }
362    
363        class ResolveAction extends AbstractAction implements ListSelectionListener {
364            public ResolveAction() {
365                putValue(NAME, tr("Resolve"));
366                putValue(SHORT_DESCRIPTION,  tr("Open a merge dialog of all selected items in the list above."));
367                putValue(SMALL_ICON, ImageProvider.get("dialogs", "conflict"));
368                putValue("help", ht("/Dialog/ConflictList#ResolveAction"));
369            }
370    
371            public void actionPerformed(ActionEvent e) {
372                resolve();
373            }
374    
375            public void valueChanged(ListSelectionEvent e) {
376                ListSelectionModel model = (ListSelectionModel)e.getSource();
377                boolean enabled = model.getMinSelectionIndex() >= 0
378                && model.getMaxSelectionIndex() >= model.getMinSelectionIndex();
379                setEnabled(enabled);
380            }
381        }
382    
383        class SelectAction extends AbstractAction implements ListSelectionListener {
384            public SelectAction() {
385                putValue(NAME, tr("Select"));
386                putValue(SHORT_DESCRIPTION,  tr("Set the selected elements on the map to the selected items in the list above."));
387                putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
388                putValue("help", ht("/Dialog/ConflictList#SelectAction"));
389            }
390    
391            public void actionPerformed(ActionEvent e) {
392                Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>();
393                for (Object o : lstConflicts.getSelectedValues()) {
394                    sel.add((OsmPrimitive)o);
395                }
396                DataSet ds = Main.main.getCurrentDataSet();
397                if (ds != null) { // Can't see how it is possible but it happened in #7942 
398                    ds.setSelected(sel);
399                }
400            }
401    
402            public void valueChanged(ListSelectionEvent e) {
403                ListSelectionModel model = (ListSelectionModel)e.getSource();
404                boolean enabled = model.getMinSelectionIndex() >= 0
405                && model.getMaxSelectionIndex() >= model.getMinSelectionIndex();
406                setEnabled(enabled);
407            }
408        }
409    
410    }