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