001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.conflict.pair.tags;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Adjustable;
007    import java.awt.GridBagConstraints;
008    import java.awt.GridBagLayout;
009    import java.awt.Insets;
010    import java.awt.event.ActionEvent;
011    import java.awt.event.AdjustmentEvent;
012    import java.awt.event.AdjustmentListener;
013    import java.awt.event.MouseAdapter;
014    import java.awt.event.MouseEvent;
015    import java.util.ArrayList;
016    
017    import javax.swing.AbstractAction;
018    import javax.swing.Action;
019    import javax.swing.ImageIcon;
020    import javax.swing.JButton;
021    import javax.swing.JLabel;
022    import javax.swing.JPanel;
023    import javax.swing.JScrollPane;
024    import javax.swing.JTable;
025    import javax.swing.event.ListSelectionEvent;
026    import javax.swing.event.ListSelectionListener;
027    
028    import org.openstreetmap.josm.data.conflict.Conflict;
029    import org.openstreetmap.josm.data.osm.OsmPrimitive;
030    import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
031    import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
032    import org.openstreetmap.josm.tools.ImageProvider;
033    /**
034     * UI component for resolving conflicts in the tag sets of two {@link OsmPrimitive}s.
035     *
036     */
037    public class TagMerger extends JPanel implements IConflictResolver {
038    
039        private JTable mineTable;
040        private JTable mergedTable;
041        private JTable theirTable;
042        private final TagMergeModel model;
043        private JButton btnKeepMine;
044        private JButton btnKeepTheir;
045        AdjustmentSynchronizer adjustmentSynchronizer;
046    
047        /**
048         * embeds table in a new {@link JScrollPane} and returns th scroll pane
049         *
050         * @param table the table
051         * @return the scroll pane embedding the table
052         */
053        protected JScrollPane embeddInScrollPane(JTable table) {
054            JScrollPane pane = new JScrollPane(table);
055            pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
056            pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
057    
058            adjustmentSynchronizer.synchronizeAdjustment(pane.getVerticalScrollBar());
059            return pane;
060        }
061    
062        /**
063         * builds the table for my tag set (table already embedded in a scroll pane)
064         *
065         * @return the table (embedded in a scroll pane)
066         */
067        protected JScrollPane buildMineTagTable() {
068            mineTable  = new JTable(
069                    model,
070                    new TagMergeColumnModel(
071                            new MineTableCellRenderer()
072                    )
073            );
074            mineTable.setName("table.my");
075            return embeddInScrollPane(mineTable);
076        }
077    
078        /**
079         * builds the table for their tag set (table already embedded in a scroll pane)
080         *
081         * @return the table (embedded in a scroll pane)
082         */
083        protected JScrollPane buildTheirTable() {
084            theirTable  = new JTable(
085                    model,
086                    new TagMergeColumnModel(
087                            new TheirTableCellRenderer()
088                    )
089            );
090            theirTable.setName("table.their");
091            return embeddInScrollPane(theirTable);
092        }
093    
094        /**
095         * builds the table for the merged tag set (table already embedded in a scroll pane)
096         *
097         * @return the table (embedded in a scroll pane)
098         */
099    
100        protected JScrollPane buildMergedTable() {
101            mergedTable  = new JTable(
102                    model,
103                    new TagMergeColumnModel(
104                            new MergedTableCellRenderer()
105                    )
106            );
107            mergedTable.setName("table.merged");
108            return embeddInScrollPane(mergedTable);
109        }
110    
111        /**
112         * build the user interface
113         */
114        protected void build() {
115            GridBagConstraints gc = new GridBagConstraints();
116            setLayout(new GridBagLayout());
117    
118            adjustmentSynchronizer = new AdjustmentSynchronizer();
119    
120            gc.gridx = 0;
121            gc.gridy = 0;
122            gc.gridwidth = 1;
123            gc.gridheight = 1;
124            gc.fill = GridBagConstraints.NONE;
125            gc.anchor = GridBagConstraints.CENTER;
126            gc.weightx = 0.0;
127            gc.weighty = 0.0;
128            gc.insets = new Insets(10,0,10,0);
129            JLabel lbl = new JLabel(tr("My version (local dataset)"));
130            add(lbl, gc);
131    
132            gc.gridx = 2;
133            gc.gridy = 0;
134            gc.gridwidth = 1;
135            gc.gridheight = 1;
136            gc.fill = GridBagConstraints.NONE;
137            gc.anchor = GridBagConstraints.CENTER;
138            gc.weightx = 0.0;
139            gc.weighty = 0.0;
140            lbl = new JLabel(tr("Merged version"));
141            add(lbl, gc);
142    
143            gc.gridx = 4;
144            gc.gridy = 0;
145            gc.gridwidth = 1;
146            gc.gridheight = 1;
147            gc.fill = GridBagConstraints.NONE;
148            gc.anchor = GridBagConstraints.CENTER;
149            gc.weightx = 0.0;
150            gc.weighty = 0.0;
151            gc.insets = new Insets(0,0,0,0);
152            lbl = new JLabel(tr("Their version (server dataset)"));
153            add(lbl, gc);
154    
155            gc.gridx = 0;
156            gc.gridy = 1;
157            gc.gridwidth = 1;
158            gc.gridheight = 1;
159            gc.fill = GridBagConstraints.BOTH;
160            gc.anchor = GridBagConstraints.FIRST_LINE_START;
161            gc.weightx = 0.3;
162            gc.weighty = 1.0;
163            add(buildMineTagTable(), gc);
164    
165            gc.gridx = 1;
166            gc.gridy = 1;
167            gc.gridwidth = 1;
168            gc.gridheight = 1;
169            gc.fill = GridBagConstraints.NONE;
170            gc.anchor = GridBagConstraints.CENTER;
171            gc.weightx = 0.0;
172            gc.weighty = 0.0;
173            KeepMineAction keepMineAction = new KeepMineAction();
174            mineTable.getSelectionModel().addListSelectionListener(keepMineAction);
175            btnKeepMine = new JButton(keepMineAction);
176            btnKeepMine.setName("button.keepmine");
177            add(btnKeepMine, gc);
178    
179            gc.gridx = 2;
180            gc.gridy = 1;
181            gc.gridwidth = 1;
182            gc.gridheight = 1;
183            gc.fill = GridBagConstraints.BOTH;
184            gc.anchor = GridBagConstraints.FIRST_LINE_START;
185            gc.weightx = 0.3;
186            gc.weighty = 1.0;
187            add(buildMergedTable(), gc);
188    
189            gc.gridx = 3;
190            gc.gridy = 1;
191            gc.gridwidth = 1;
192            gc.gridheight = 1;
193            gc.fill = GridBagConstraints.NONE;
194            gc.anchor = GridBagConstraints.CENTER;
195            gc.weightx = 0.0;
196            gc.weighty = 0.0;
197            KeepTheirAction keepTheirAction = new KeepTheirAction();
198            btnKeepTheir = new JButton(keepTheirAction);
199            btnKeepTheir.setName("button.keeptheir");
200            add(btnKeepTheir, gc);
201    
202            gc.gridx = 4;
203            gc.gridy = 1;
204            gc.gridwidth = 1;
205            gc.gridheight = 1;
206            gc.fill = GridBagConstraints.BOTH;
207            gc.anchor = GridBagConstraints.FIRST_LINE_START;
208            gc.weightx = 0.3;
209            gc.weighty = 1.0;
210            add(buildTheirTable(), gc);
211            theirTable.getSelectionModel().addListSelectionListener(keepTheirAction);
212    
213            DoubleClickAdapter dblClickAdapter = new DoubleClickAdapter();
214            mineTable.addMouseListener(dblClickAdapter);
215            theirTable.addMouseListener(dblClickAdapter);
216    
217            gc.gridx = 2;
218            gc.gridy = 2;
219            gc.gridwidth = 1;
220            gc.gridheight = 1;
221            gc.fill = GridBagConstraints.NONE;
222            gc.anchor = GridBagConstraints.CENTER;
223            gc.weightx = 0.0;
224            gc.weighty = 0.0;
225            UndecideAction undecidedAction = new UndecideAction();
226            mergedTable.getSelectionModel().addListSelectionListener(undecidedAction);
227            JButton btnUndecide = new JButton(undecidedAction);
228            btnUndecide.setName("button.undecide");
229            add(btnUndecide, gc);
230    
231        }
232    
233        public TagMerger() {
234            model = new TagMergeModel();
235            build();
236        }
237    
238        /**
239         * replies the model used by this tag merger
240         *
241         * @return the model
242         */
243        public TagMergeModel getModel() {
244            return model;
245        }
246    
247        private void selectNextConflict(int[] rows) {
248            int max = rows[0];
249            for (int row: rows) {
250                if (row > max) {
251                    max = row;
252                }
253            }
254            int index = model.getFirstUndecided(max+1);
255            if (index == -1) {
256                index = model.getFirstUndecided(0);
257            }
258            mineTable.getSelectionModel().setSelectionInterval(index, index);
259            theirTable.getSelectionModel().setSelectionInterval(index, index);
260        }
261    
262        /**
263         * Keeps the currently selected tags in my table in the list of merged tags.
264         *
265         */
266        class KeepMineAction extends AbstractAction implements ListSelectionListener {
267            public KeepMineAction() {
268                ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeepmine.png");
269                if (icon != null) {
270                    putValue(Action.SMALL_ICON, icon);
271                    putValue(Action.NAME, "");
272                } else {
273                    putValue(Action.NAME, ">");
274                }
275                putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the local dataset"));
276                setEnabled(false);
277            }
278    
279            public void actionPerformed(ActionEvent arg0) {
280                int rows[] = mineTable.getSelectedRows();
281                if (rows == null || rows.length == 0)
282                    return;
283                model.decide(rows, MergeDecisionType.KEEP_MINE);
284                selectNextConflict(rows);
285            }
286    
287            public void valueChanged(ListSelectionEvent e) {
288                setEnabled(mineTable.getSelectedRowCount() > 0);
289            }
290        }
291    
292        /**
293         * Keeps the currently selected tags in their table in the list of merged tags.
294         *
295         */
296        class KeepTheirAction extends AbstractAction implements ListSelectionListener {
297            public KeepTheirAction() {
298                ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagkeeptheir.png");
299                if (icon != null) {
300                    putValue(Action.SMALL_ICON, icon);
301                    putValue(Action.NAME, "");
302                } else {
303                    putValue(Action.NAME, ">");
304                }
305                putValue(Action.SHORT_DESCRIPTION, tr("Keep the selected key/value pairs from the server dataset"));
306                setEnabled(false);
307            }
308    
309            public void actionPerformed(ActionEvent arg0) {
310                int rows[] = theirTable.getSelectedRows();
311                if (rows == null || rows.length == 0)
312                    return;
313                model.decide(rows, MergeDecisionType.KEEP_THEIR);
314                selectNextConflict(rows);
315            }
316    
317            public void valueChanged(ListSelectionEvent e) {
318                setEnabled(theirTable.getSelectedRowCount() > 0);
319            }
320        }
321    
322        /**
323         * Synchronizes scrollbar adjustments between a set of
324         * {@link Adjustable}s. Whenever the adjustment of one of
325         * the registerd Adjustables is updated the adjustment of
326         * the other registered Adjustables is adjusted too.
327         *
328         */
329        static class AdjustmentSynchronizer implements AdjustmentListener {
330            private final ArrayList<Adjustable> synchronizedAdjustables;
331    
332            public AdjustmentSynchronizer() {
333                synchronizedAdjustables = new ArrayList<Adjustable>();
334            }
335    
336            public void synchronizeAdjustment(Adjustable adjustable) {
337                if (adjustable == null)
338                    return;
339                if (synchronizedAdjustables.contains(adjustable))
340                    return;
341                synchronizedAdjustables.add(adjustable);
342                adjustable.addAdjustmentListener(this);
343            }
344    
345            public void adjustmentValueChanged(AdjustmentEvent e) {
346                for (Adjustable a : synchronizedAdjustables) {
347                    if (a != e.getAdjustable()) {
348                        a.setValue(e.getValue());
349                    }
350                }
351            }
352        }
353    
354        /**
355         * Handler for double clicks on entries in the three tag tables.
356         *
357         */
358        class DoubleClickAdapter extends MouseAdapter {
359    
360            @Override
361            public void mouseClicked(MouseEvent e) {
362                if (e.getClickCount() != 2)
363                    return;
364                JTable table = null;
365                MergeDecisionType mergeDecision;
366    
367                if (e.getSource() == mineTable) {
368                    table = mineTable;
369                    mergeDecision = MergeDecisionType.KEEP_MINE;
370                } else if (e.getSource() == theirTable) {
371                    table = theirTable;
372                    mergeDecision = MergeDecisionType.KEEP_THEIR;
373                } else if (e.getSource() == mergedTable) {
374                    table = mergedTable;
375                    mergeDecision = MergeDecisionType.UNDECIDED;
376                } else
377                    // double click in another component; shouldn't happen,
378                    // but just in case
379                    return;
380                int row = table.rowAtPoint(e.getPoint());
381                model.decide(row, mergeDecision);
382            }
383        }
384    
385        /**
386         * Sets the currently selected tags in the table of merged tags to state
387         * {@link MergeDecisionType#UNDECIDED}
388         *
389         */
390        class UndecideAction extends AbstractAction implements ListSelectionListener  {
391    
392            public UndecideAction() {
393                ImageIcon icon = ImageProvider.get("dialogs/conflict", "tagundecide.png");
394                if (icon != null) {
395                    putValue(Action.SMALL_ICON, icon);
396                    putValue(Action.NAME, "");
397                } else {
398                    putValue(Action.NAME, tr("Undecide"));
399                }
400                putValue(SHORT_DESCRIPTION, tr("Mark the selected tags as undecided"));
401                setEnabled(false);
402            }
403    
404            public void actionPerformed(ActionEvent arg0) {
405                int rows[] = mergedTable.getSelectedRows();
406                if (rows == null || rows.length == 0)
407                    return;
408                model.decide(rows, MergeDecisionType.UNDECIDED);
409            }
410    
411            public void valueChanged(ListSelectionEvent e) {
412                setEnabled(mergedTable.getSelectedRowCount() > 0);
413            }
414        }
415    
416        public void deletePrimitive(boolean deleted) {
417            // Use my entries, as it doesn't really matter
418            MergeDecisionType decision = deleted?MergeDecisionType.KEEP_MINE:MergeDecisionType.UNDECIDED;
419            for (int i=0; i<model.getRowCount(); i++) {
420                model.decide(i, decision);
421            }
422        }
423    
424        public void populate(Conflict<? extends OsmPrimitive> conflict) {
425            model.populate(conflict.getMy(), conflict.getTheir());
426            mineTable.getSelectionModel().setSelectionInterval(0, 0);
427            theirTable.getSelectionModel().setSelectionInterval(0, 0);
428        }
429    }