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