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