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