001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.conflict.pair;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trn;
006    
007    import java.awt.Adjustable;
008    import java.awt.FlowLayout;
009    import java.awt.GridBagConstraints;
010    import java.awt.GridBagLayout;
011    import java.awt.Insets;
012    import java.awt.event.ActionEvent;
013    import java.awt.event.AdjustmentEvent;
014    import java.awt.event.AdjustmentListener;
015    import java.awt.event.ItemEvent;
016    import java.awt.event.ItemListener;
017    import java.beans.PropertyChangeEvent;
018    import java.beans.PropertyChangeListener;
019    import java.util.ArrayList;
020    import java.util.Collection;
021    import java.util.HashMap;
022    import java.util.List;
023    import java.util.Observable;
024    import java.util.Observer;
025    
026    import javax.swing.AbstractAction;
027    import javax.swing.Action;
028    import javax.swing.ImageIcon;
029    import javax.swing.JButton;
030    import javax.swing.JCheckBox;
031    import javax.swing.JLabel;
032    import javax.swing.JPanel;
033    import javax.swing.JScrollPane;
034    import javax.swing.JTable;
035    import javax.swing.JToggleButton;
036    import javax.swing.event.ListSelectionEvent;
037    import javax.swing.event.ListSelectionListener;
038    
039    import org.openstreetmap.josm.Main;
040    import org.openstreetmap.josm.data.osm.OsmPrimitive;
041    import org.openstreetmap.josm.data.osm.PrimitiveId;
042    import org.openstreetmap.josm.data.osm.Relation;
043    import org.openstreetmap.josm.data.osm.Way;
044    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
045    import org.openstreetmap.josm.gui.widgets.JosmComboBox;
046    import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
047    import org.openstreetmap.josm.tools.CheckParameterUtil;
048    import org.openstreetmap.josm.tools.ImageProvider;
049    
050    /**
051     * A UI component for resolving conflicts in two lists of entries of type T.
052     *
053     * @param T  the type of the entries
054     * @see ListMergeModel
055     */
056    public abstract class ListMerger<T extends PrimitiveId> extends JPanel implements PropertyChangeListener, Observer {
057        protected OsmPrimitivesTable myEntriesTable;
058        protected OsmPrimitivesTable mergedEntriesTable;
059        protected OsmPrimitivesTable theirEntriesTable;
060    
061        protected ListMergeModel<T> model;
062    
063        private CopyStartLeftAction copyStartLeftAction;
064        private CopyBeforeCurrentLeftAction copyBeforeCurrentLeftAction;
065        private CopyAfterCurrentLeftAction copyAfterCurrentLeftAction;
066        private CopyEndLeftAction copyEndLeftAction;
067        private CopyAllLeft copyAllLeft;
068    
069        private CopyStartRightAction copyStartRightAction;
070        private CopyBeforeCurrentRightAction copyBeforeCurrentRightAction;
071        private CopyAfterCurrentRightAction copyAfterCurrentRightAction;
072        private CopyEndRightAction copyEndRightAction;
073        private CopyAllRight copyAllRight;
074    
075        private MoveUpMergedAction moveUpMergedAction;
076        private MoveDownMergedAction moveDownMergedAction;
077        private RemoveMergedAction removeMergedAction;
078        private FreezeAction freezeAction;
079    
080        private AdjustmentSynchronizer adjustmentSynchronizer;
081    
082        private  JCheckBox cbLockMyScrolling;
083        private  JCheckBox cbLockMergedScrolling;
084        private  JCheckBox cbLockTheirScrolling;
085    
086        private  JLabel lblMyVersion;
087        private  JLabel lblMergedVersion;
088        private  JLabel lblTheirVersion;
089    
090        private  JLabel lblFrozenState;
091    
092        abstract protected JScrollPane buildMyElementsTable();
093        abstract protected JScrollPane buildMergedElementsTable();
094        abstract protected JScrollPane buildTheirElementsTable();
095    
096        protected JScrollPane embeddInScrollPane(JTable table) {
097            JScrollPane pane = new JScrollPane(table);
098            pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
099            pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
100            if (adjustmentSynchronizer == null) {
101                adjustmentSynchronizer = new AdjustmentSynchronizer();
102            }
103            return pane;
104        }
105    
106        protected void wireActionsToSelectionModels() {
107            myEntriesTable.getSelectionModel().addListSelectionListener(copyStartLeftAction);
108    
109            myEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
110            mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
111    
112            myEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
113            mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
114    
115            myEntriesTable.getSelectionModel().addListSelectionListener(copyEndLeftAction);
116    
117            theirEntriesTable.getSelectionModel().addListSelectionListener(copyStartRightAction);
118    
119            theirEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
120            mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
121    
122            theirEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
123            mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
124    
125            theirEntriesTable.getSelectionModel().addListSelectionListener(copyEndRightAction);
126    
127            mergedEntriesTable.getSelectionModel().addListSelectionListener(moveUpMergedAction);
128            mergedEntriesTable.getSelectionModel().addListSelectionListener(moveDownMergedAction);
129            mergedEntriesTable.getSelectionModel().addListSelectionListener(removeMergedAction);
130    
131            model.addObserver(copyAllLeft);
132            model.addObserver(copyAllRight);
133            model.addPropertyChangeListener(copyAllLeft);
134            model.addPropertyChangeListener(copyAllRight);
135        }
136    
137        protected JPanel buildLeftButtonPanel() {
138            JPanel pnl = new JPanel();
139            pnl.setLayout(new GridBagLayout());
140            GridBagConstraints gc = new GridBagConstraints();
141    
142            gc.gridx = 0;
143            gc.gridy = 0;
144            copyStartLeftAction = new CopyStartLeftAction();
145            JButton btn = new JButton(copyStartLeftAction);
146            btn.setName("button.copystartleft");
147            pnl.add(btn, gc);
148    
149            gc.gridx = 0;
150            gc.gridy = 1;
151            copyBeforeCurrentLeftAction = new CopyBeforeCurrentLeftAction();
152            btn = new JButton(copyBeforeCurrentLeftAction);
153            btn.setName("button.copybeforecurrentleft");
154            pnl.add(btn, gc);
155    
156            gc.gridx = 0;
157            gc.gridy = 2;
158            copyAfterCurrentLeftAction = new CopyAfterCurrentLeftAction();
159            btn = new JButton(copyAfterCurrentLeftAction);
160            btn.setName("button.copyaftercurrentleft");
161            pnl.add(btn, gc);
162    
163            gc.gridx = 0;
164            gc.gridy = 3;
165            copyEndLeftAction = new CopyEndLeftAction();
166            btn = new JButton(copyEndLeftAction);
167            btn.setName("button.copyendleft");
168            pnl.add(btn, gc);
169    
170            gc.gridx = 0;
171            gc.gridy = 4;
172            copyAllLeft = new CopyAllLeft();
173            btn = new JButton(copyAllLeft);
174            btn.setName("button.copyallleft");
175            pnl.add(btn, gc);
176    
177            return pnl;
178        }
179    
180        protected JPanel buildRightButtonPanel() {
181            JPanel pnl = new JPanel();
182            pnl.setLayout(new GridBagLayout());
183            GridBagConstraints gc = new GridBagConstraints();
184    
185            gc.gridx = 0;
186            gc.gridy = 0;
187            copyStartRightAction = new CopyStartRightAction();
188            pnl.add(new JButton(copyStartRightAction), gc);
189    
190            gc.gridx = 0;
191            gc.gridy = 1;
192            copyBeforeCurrentRightAction = new CopyBeforeCurrentRightAction();
193            pnl.add(new JButton(copyBeforeCurrentRightAction), gc);
194    
195            gc.gridx = 0;
196            gc.gridy = 2;
197            copyAfterCurrentRightAction = new CopyAfterCurrentRightAction();
198            pnl.add(new JButton(copyAfterCurrentRightAction), gc);
199    
200            gc.gridx = 0;
201            gc.gridy = 3;
202            copyEndRightAction = new CopyEndRightAction();
203            pnl.add(new JButton(copyEndRightAction), gc);
204    
205            gc.gridx = 0;
206            gc.gridy = 4;
207            copyAllRight = new CopyAllRight();
208            pnl.add(new JButton(copyAllRight), gc);
209    
210            return pnl;
211        }
212    
213        protected JPanel buildMergedListControlButtons() {
214            JPanel pnl = new JPanel();
215            pnl.setLayout(new GridBagLayout());
216            GridBagConstraints gc = new GridBagConstraints();
217    
218            gc.gridx = 0;
219            gc.gridy = 0;
220            gc.gridwidth = 1;
221            gc.gridheight = 1;
222            gc.fill = GridBagConstraints.HORIZONTAL;
223            gc.anchor = GridBagConstraints.CENTER;
224            gc.weightx = 0.3;
225            gc.weighty = 0.0;
226            moveUpMergedAction = new MoveUpMergedAction();
227            pnl.add(new JButton(moveUpMergedAction), gc);
228    
229            gc.gridx = 1;
230            gc.gridy = 0;
231            moveDownMergedAction = new MoveDownMergedAction();
232            pnl.add(new JButton(moveDownMergedAction), gc);
233    
234            gc.gridx = 2;
235            gc.gridy = 0;
236            removeMergedAction = new RemoveMergedAction();
237            pnl.add(new JButton(removeMergedAction), gc);
238    
239            return pnl;
240        }
241    
242        protected JPanel buildAdjustmentLockControlPanel(JCheckBox cb) {
243            JPanel panel = new JPanel();
244            panel.setLayout(new FlowLayout(FlowLayout.RIGHT));
245            panel.add(new JLabel(tr("lock scrolling")));
246            panel.add(cb);
247            return panel;
248        }
249    
250        protected JPanel buildComparePairSelectionPanel() {
251            JPanel p = new JPanel();
252            p.setLayout(new FlowLayout(FlowLayout.LEFT));
253            p.add(new JLabel(tr("Compare ")));
254            JosmComboBox cbComparePair = new JosmComboBox(model.getComparePairListModel());
255            cbComparePair.setRenderer(new ComparePairListCellRenderer());
256            p.add(cbComparePair);
257            return p;
258        }
259    
260        protected JPanel buildFrozeStateControlPanel() {
261            JPanel p = new JPanel();
262            p.setLayout(new FlowLayout(FlowLayout.LEFT));
263            lblFrozenState = new JLabel();
264            p.add(lblFrozenState);
265            freezeAction = new FreezeAction();
266            JToggleButton btn = new JToggleButton(freezeAction);
267            freezeAction.adapt(btn);
268            btn.setName("button.freeze");
269            p.add(btn);
270    
271            return p;
272        }
273    
274        protected void build() {
275            setLayout(new GridBagLayout());
276            GridBagConstraints gc = new GridBagConstraints();
277    
278            // ------------------
279            gc.gridx = 0;
280            gc.gridy = 0;
281            gc.gridwidth = 1;
282            gc.gridheight = 1;
283            gc.fill = GridBagConstraints.NONE;
284            gc.anchor = GridBagConstraints.CENTER;
285            gc.weightx = 0.0;
286            gc.weighty = 0.0;
287            gc.insets = new Insets(10,0,0,0);
288            lblMyVersion = new JLabel(tr("My version"));
289            lblMyVersion.setToolTipText(tr("List of elements in my dataset, i.e. the local dataset"));
290            add(lblMyVersion, gc);
291    
292            gc.gridx = 2;
293            gc.gridy = 0;
294            lblMergedVersion = new JLabel(tr("Merged version"));
295            lblMergedVersion.setToolTipText(tr("List of merged elements. They will replace the list of my elements when the merge decisions are applied."));
296            add(lblMergedVersion, gc);
297    
298            gc.gridx = 4;
299            gc.gridy = 0;
300            lblTheirVersion = new JLabel(tr("Their version"));
301            lblTheirVersion.setToolTipText(tr("List of elements in their dataset, i.e. the server dataset"));
302            add(lblTheirVersion, gc);
303    
304            // ------------------------------
305            gc.gridx = 0;
306            gc.gridy = 1;
307            gc.gridwidth = 1;
308            gc.gridheight = 1;
309            gc.fill = GridBagConstraints.HORIZONTAL;
310            gc.anchor = GridBagConstraints.FIRST_LINE_START;
311            gc.weightx = 0.33;
312            gc.weighty = 0.0;
313            gc.insets = new Insets(0,0,0,0);
314            cbLockMyScrolling = new JCheckBox();
315            cbLockMyScrolling.setName("checkbox.lockmyscrolling");
316            add(buildAdjustmentLockControlPanel(cbLockMyScrolling), gc);
317    
318            gc.gridx = 2;
319            gc.gridy = 1;
320            cbLockMergedScrolling = new JCheckBox();
321            cbLockMergedScrolling.setName("checkbox.lockmergedscrolling");
322            add(buildAdjustmentLockControlPanel(cbLockMergedScrolling), gc);
323    
324            gc.gridx = 4;
325            gc.gridy = 1;
326            cbLockTheirScrolling = new JCheckBox();
327            cbLockTheirScrolling.setName("checkbox.locktheirscrolling");
328            add(buildAdjustmentLockControlPanel(cbLockTheirScrolling), gc);
329    
330            // --------------------------------
331            gc.gridx = 0;
332            gc.gridy = 2;
333            gc.gridwidth = 1;
334            gc.gridheight = 1;
335            gc.fill = GridBagConstraints.BOTH;
336            gc.anchor = GridBagConstraints.FIRST_LINE_START;
337            gc.weightx = 0.33;
338            gc.weighty = 1.0;
339            gc.insets = new Insets(0,0,0,0);
340            JScrollPane pane = buildMyElementsTable();
341            adjustmentSynchronizer.adapt(cbLockMyScrolling, pane.getVerticalScrollBar());
342            add(pane, gc);
343    
344            gc.gridx = 1;
345            gc.gridy = 2;
346            gc.fill = GridBagConstraints.NONE;
347            gc.anchor = GridBagConstraints.CENTER;
348            gc.weightx = 0.0;
349            gc.weighty = 0.0;
350            add(buildLeftButtonPanel(), gc);
351    
352            gc.gridx = 2;
353            gc.gridy = 2;
354            gc.fill = GridBagConstraints.BOTH;
355            gc.anchor = GridBagConstraints.FIRST_LINE_START;
356            gc.weightx = 0.33;
357            gc.weighty = 0.0;
358            pane = buildMergedElementsTable();
359            adjustmentSynchronizer.adapt(cbLockMergedScrolling, pane.getVerticalScrollBar());
360            add(pane, gc);
361    
362            gc.gridx = 3;
363            gc.gridy = 2;
364            gc.fill = GridBagConstraints.NONE;
365            gc.anchor = GridBagConstraints.CENTER;
366            gc.weightx = 0.0;
367            gc.weighty = 0.0;
368            add(buildRightButtonPanel(), gc);
369    
370            gc.gridx = 4;
371            gc.gridy = 2;
372            gc.fill = GridBagConstraints.BOTH;
373            gc.anchor = GridBagConstraints.FIRST_LINE_START;
374            gc.weightx = 0.33;
375            gc.weighty = 0.0;
376            pane = buildTheirElementsTable();
377            adjustmentSynchronizer.adapt(cbLockTheirScrolling, pane.getVerticalScrollBar());
378            add(pane, gc);
379    
380            // ----------------------------------
381            gc.gridx = 2;
382            gc.gridy = 3;
383            gc.gridwidth = 1;
384            gc.gridheight = 1;
385            gc.fill = GridBagConstraints.BOTH;
386            gc.anchor = GridBagConstraints.CENTER;
387            gc.weightx = 0.0;
388            gc.weighty = 0.0;
389            add(buildMergedListControlButtons(), gc);
390    
391            // -----------------------------------
392            gc.gridx = 0;
393            gc.gridy = 4;
394            gc.gridwidth = 2;
395            gc.gridheight = 1;
396            gc.fill = GridBagConstraints.HORIZONTAL;
397            gc.anchor = GridBagConstraints.LINE_START;
398            gc.weightx = 0.0;
399            gc.weighty = 0.0;
400            add(buildComparePairSelectionPanel(), gc);
401    
402            gc.gridx = 2;
403            gc.gridy = 4;
404            gc.gridwidth = 3;
405            gc.gridheight = 1;
406            gc.fill = GridBagConstraints.HORIZONTAL;
407            gc.anchor = GridBagConstraints.LINE_START;
408            gc.weightx = 0.0;
409            gc.weighty = 0.0;
410            add(buildFrozeStateControlPanel(), gc);
411    
412            wireActionsToSelectionModels();
413        }
414    
415        public ListMerger(ListMergeModel<T> model) {
416            this.model = model;
417            model.addObserver(this);
418            build();
419            model.addPropertyChangeListener(this);
420        }
421    
422        /**
423         * Action for copying selected nodes in the list of my nodes to the list of merged
424         * nodes. Inserts the nodes at the beginning of the list of merged nodes.
425         *
426         */
427        class CopyStartLeftAction extends AbstractAction implements ListSelectionListener {
428    
429            public CopyStartLeftAction() {
430                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copystartleft.png");
431                putValue(Action.SMALL_ICON, icon);
432                if (icon == null) {
433                    putValue(Action.NAME, tr("> top"));
434                }
435                putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected nodes to the start of the merged node list"));
436                setEnabled(false);
437            }
438    
439            public void actionPerformed(ActionEvent arg0) {
440                int [] rows = myEntriesTable.getSelectedRows();
441                model.copyMyToTop(rows);
442            }
443    
444            public void valueChanged(ListSelectionEvent e) {
445                setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
446            }
447        }
448    
449        /**
450         * Action for copying selected nodes in the list of my nodes to the list of merged
451         * nodes. Inserts the nodes at the end of the list of merged nodes.
452         *
453         */
454        class CopyEndLeftAction extends AbstractAction implements ListSelectionListener {
455    
456            public CopyEndLeftAction() {
457                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyendleft.png");
458                putValue(Action.SMALL_ICON, icon);
459                if (icon == null) {
460                    putValue(Action.NAME, tr("> bottom"));
461                }
462                putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements to the end of the list of merged elements."));
463                setEnabled(false);
464            }
465    
466            public void actionPerformed(ActionEvent arg0) {
467                int [] rows = myEntriesTable.getSelectedRows();
468                model.copyMyToEnd(rows);
469            }
470    
471            public void valueChanged(ListSelectionEvent e) {
472                setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
473            }
474        }
475    
476        /**
477         * Action for copying selected nodes in the list of my nodes to the list of merged
478         * nodes. Inserts the nodes before the first selected row in the list of merged nodes.
479         *
480         */
481        class CopyBeforeCurrentLeftAction extends AbstractAction implements ListSelectionListener {
482    
483            public CopyBeforeCurrentLeftAction() {
484                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copybeforecurrentleft.png");
485                putValue(Action.SMALL_ICON, icon);
486                if (icon == null) {
487                    putValue(Action.NAME, "> before");
488                }
489                putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements before the first selected element in the list of merged elements."));
490                setEnabled(false);
491            }
492    
493            public void actionPerformed(ActionEvent arg0) {
494                int [] myRows = myEntriesTable.getSelectedRows();
495                int [] mergedRows = mergedEntriesTable.getSelectedRows();
496                if (mergedRows == null || mergedRows.length == 0)
497                    return;
498                int current = mergedRows[0];
499                model.copyMyBeforeCurrent(myRows, current);
500            }
501    
502            public void valueChanged(ListSelectionEvent e) {
503                setEnabled(
504                        !myEntriesTable.getSelectionModel().isSelectionEmpty()
505                        && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
506                );
507            }
508        }
509    
510        /**
511         * Action for copying selected nodes in the list of my nodes to the list of merged
512         * nodes. Inserts the nodes after the first selected row in the list of merged nodes.
513         *
514         */
515        class CopyAfterCurrentLeftAction extends AbstractAction implements ListSelectionListener {
516    
517            public CopyAfterCurrentLeftAction() {
518                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyaftercurrentleft.png");
519                putValue(Action.SMALL_ICON, icon);
520                if (icon == null) {
521                    putValue(Action.NAME, "> after");
522                }
523                putValue(Action.SHORT_DESCRIPTION, tr("Copy my selected elements after the first selected element in the list of merged elements."));
524                setEnabled(false);
525            }
526    
527            public void actionPerformed(ActionEvent arg0) {
528                int [] myRows = myEntriesTable.getSelectedRows();
529                int [] mergedRows = mergedEntriesTable.getSelectedRows();
530                if (mergedRows == null || mergedRows.length == 0)
531                    return;
532                int current = mergedRows[0];
533                model.copyMyAfterCurrent(myRows, current);
534            }
535    
536            public void valueChanged(ListSelectionEvent e) {
537                setEnabled(
538                        !myEntriesTable.getSelectionModel().isSelectionEmpty()
539                        && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
540                );
541            }
542        }
543    
544        class CopyStartRightAction extends AbstractAction implements ListSelectionListener {
545    
546            public CopyStartRightAction() {
547                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copystartright.png");
548                putValue(Action.SMALL_ICON, icon);
549                if (icon == null) {
550                    putValue(Action.NAME, "< top");
551                }
552                putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected element to the start of the list of merged elements."));
553                setEnabled(false);
554            }
555    
556            public void actionPerformed(ActionEvent arg0) {
557                int [] rows = theirEntriesTable.getSelectedRows();
558                model.copyTheirToTop(rows);
559            }
560    
561            public void valueChanged(ListSelectionEvent e) {
562                setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
563            }
564        }
565    
566        class CopyEndRightAction extends AbstractAction implements ListSelectionListener {
567    
568            public CopyEndRightAction() {
569                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyendright.png");
570                putValue(Action.SMALL_ICON, icon);
571                if (icon == null) {
572                    putValue(Action.NAME, "< bottom");
573                }
574                putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected elements to the end of the list of merged elements."));
575                setEnabled(false);
576            }
577    
578            public void actionPerformed(ActionEvent arg0) {
579                int [] rows = theirEntriesTable.getSelectedRows();
580                model.copyTheirToEnd(rows);
581            }
582    
583            public void valueChanged(ListSelectionEvent e) {
584                setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
585            }
586        }
587    
588        class CopyBeforeCurrentRightAction extends AbstractAction implements ListSelectionListener {
589    
590            public CopyBeforeCurrentRightAction() {
591                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copybeforecurrentright.png");
592                putValue(Action.SMALL_ICON, icon);
593                if (icon == null) {
594                    putValue(Action.NAME, "< before");
595                }
596                putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected elements before the first selected element in the list of merged elements."));
597                setEnabled(false);
598            }
599    
600            public void actionPerformed(ActionEvent arg0) {
601                int [] myRows = theirEntriesTable.getSelectedRows();
602                int [] mergedRows = mergedEntriesTable.getSelectedRows();
603                if (mergedRows == null || mergedRows.length == 0)
604                    return;
605                int current = mergedRows[0];
606                model.copyTheirBeforeCurrent(myRows, current);
607            }
608    
609            public void valueChanged(ListSelectionEvent e) {
610                setEnabled(
611                        !theirEntriesTable.getSelectionModel().isSelectionEmpty()
612                        && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
613                );
614            }
615        }
616    
617        class CopyAfterCurrentRightAction extends AbstractAction implements ListSelectionListener {
618    
619            public CopyAfterCurrentRightAction() {
620                ImageIcon icon = ImageProvider.get("dialogs/conflict", "copyaftercurrentright.png");
621                putValue(Action.SMALL_ICON, icon);
622                if (icon == null) {
623                    putValue(Action.NAME, "< after");
624                }
625                putValue(Action.SHORT_DESCRIPTION, tr("Copy their selected element after the first selected element in the list of merged elements"));
626                setEnabled(false);
627            }
628    
629            public void actionPerformed(ActionEvent arg0) {
630                int [] myRows = theirEntriesTable.getSelectedRows();
631                int [] mergedRows = mergedEntriesTable.getSelectedRows();
632                if (mergedRows == null || mergedRows.length == 0)
633                    return;
634                int current = mergedRows[0];
635                model.copyTheirAfterCurrent(myRows, current);
636            }
637    
638            public void valueChanged(ListSelectionEvent e) {
639                setEnabled(
640                        !theirEntriesTable.getSelectionModel().isSelectionEmpty()
641                        && ! mergedEntriesTable.getSelectionModel().isSelectionEmpty()
642                );
643            }
644        }
645    
646        class CopyAllLeft extends AbstractAction implements Observer, PropertyChangeListener {
647    
648            public CopyAllLeft() {
649                ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallleft.png");
650                putValue(Action.SMALL_ICON, icon);
651                putValue(Action.SHORT_DESCRIPTION, tr("Copy all my elements to the target"));
652            }
653    
654            public void actionPerformed(ActionEvent arg0) {
655                model.copyAll(ListRole.MY_ENTRIES);
656                model.setFrozen(true);
657            }
658    
659            private void updateEnabledState() {
660                setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
661            }
662    
663            public void update(Observable o, Object arg) {
664                updateEnabledState();
665            }
666    
667            public void propertyChange(PropertyChangeEvent evt) {
668                updateEnabledState();
669            }
670        }
671    
672        class CopyAllRight extends AbstractAction implements Observer, PropertyChangeListener {
673    
674            public CopyAllRight() {
675                ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallright.png");
676                putValue(Action.SMALL_ICON, icon);
677                putValue(Action.SHORT_DESCRIPTION, tr("Copy all their elements to the target"));
678            }
679    
680            public void actionPerformed(ActionEvent arg0) {
681                model.copyAll(ListRole.THEIR_ENTRIES);
682                model.setFrozen(true);
683            }
684    
685            private void updateEnabledState() {
686                setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
687            }
688    
689            public void update(Observable o, Object arg) {
690                updateEnabledState();
691            }
692    
693            public void propertyChange(PropertyChangeEvent evt) {
694                updateEnabledState();
695            }
696        }
697    
698        class MoveUpMergedAction extends AbstractAction implements ListSelectionListener {
699    
700            public MoveUpMergedAction() {
701                ImageIcon icon = ImageProvider.get("dialogs/conflict", "moveup.png");
702                putValue(Action.SMALL_ICON, icon);
703                if (icon == null) {
704                    putValue(Action.NAME, tr("Up"));
705                }
706                putValue(Action.SHORT_DESCRIPTION, tr("Move up the selected elements by one position."));
707                setEnabled(false);
708            }
709    
710            public void actionPerformed(ActionEvent arg0) {
711                int [] rows = mergedEntriesTable.getSelectedRows();
712                model.moveUpMerged(rows);
713            }
714    
715            public void valueChanged(ListSelectionEvent e) {
716                int [] rows = mergedEntriesTable.getSelectedRows();
717                setEnabled(
718                        rows != null
719                        && rows.length > 0
720                        && rows[0] != 0
721                );
722            }
723        }
724    
725        /**
726         * Action for moving the currently selected entries in the list of merged entries
727         * one position down
728         *
729         */
730        class MoveDownMergedAction extends AbstractAction implements ListSelectionListener {
731    
732            public MoveDownMergedAction() {
733                ImageIcon icon = ImageProvider.get("dialogs/conflict", "movedown.png");
734                putValue(Action.SMALL_ICON, icon);
735                if (icon == null) {
736                    putValue(Action.NAME, tr("Down"));
737                }
738                putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected entries by one position."));
739                setEnabled(false);
740            }
741    
742            public void actionPerformed(ActionEvent arg0) {
743                int [] rows = mergedEntriesTable.getSelectedRows();
744                model.moveDownMerged(rows);
745            }
746    
747            public void valueChanged(ListSelectionEvent e) {
748                int [] rows = mergedEntriesTable.getSelectedRows();
749                setEnabled(
750                        rows != null
751                        && rows.length > 0
752                        && rows[rows.length -1] != mergedEntriesTable.getRowCount() -1
753                );
754            }
755        }
756    
757        /**
758         * Action for removing the selected entries in the list of merged entries
759         * from the list of merged entries.
760         *
761         */
762        class RemoveMergedAction extends AbstractAction implements ListSelectionListener {
763    
764            public RemoveMergedAction() {
765                ImageIcon icon = ImageProvider.get("dialogs/conflict", "remove.png");
766                putValue(Action.SMALL_ICON, icon);
767                if (icon == null) {
768                    putValue(Action.NAME, tr("Remove"));
769                }
770                putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected entries from the list of merged elements."));
771                setEnabled(false);
772            }
773    
774            public void actionPerformed(ActionEvent arg0) {
775                int [] rows = mergedEntriesTable.getSelectedRows();
776                model.removeMerged(rows);
777            }
778    
779            public void valueChanged(ListSelectionEvent e) {
780                int [] rows = mergedEntriesTable.getSelectedRows();
781                setEnabled(
782                        rows != null
783                        && rows.length > 0
784                );
785            }
786        }
787    
788        static public interface FreezeActionProperties {
789            String PROP_SELECTED = FreezeActionProperties.class.getName() + ".selected";
790        }
791    
792        /**
793         * Action for freezing the current state of the list merger
794         *
795         */
796        class FreezeAction extends AbstractAction implements ItemListener, FreezeActionProperties  {
797    
798            public FreezeAction() {
799                putValue(Action.NAME, tr("Freeze"));
800                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
801                putValue(PROP_SELECTED, false);
802                setEnabled(true);
803            }
804    
805            public void actionPerformed(ActionEvent arg0) {
806                // do nothing
807            }
808    
809            /**
810             * Java 1.5 doesn't known Action.SELECT_KEY. Wires a toggle button to this action
811             * such that the action gets notified about item state changes and the button gets
812             * notified about selection state changes of the action.
813             *
814             * @param btn a toggle button
815             */
816            public void adapt(final JToggleButton btn) {
817                btn.addItemListener(this);
818                addPropertyChangeListener(
819                        new PropertyChangeListener() {
820                            public void propertyChange(PropertyChangeEvent evt) {
821                                if (evt.getPropertyName().equals(PROP_SELECTED)) {
822                                    btn.setSelected((Boolean)evt.getNewValue());
823                                }
824                            }
825                        }
826                );
827            }
828    
829            public void itemStateChanged(ItemEvent e) {
830                int state = e.getStateChange();
831                if (state == ItemEvent.SELECTED) {
832                    putValue(Action.NAME, tr("Unfreeze"));
833                    putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged elements and start merging."));
834                    model.setFrozen(true);
835                } else if (state == ItemEvent.DESELECTED) {
836                    putValue(Action.NAME, tr("Freeze"));
837                    putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
838                    model.setFrozen(false);
839                }
840                boolean isSelected = (Boolean)getValue(PROP_SELECTED);
841                if (isSelected != (e.getStateChange() == ItemEvent.SELECTED)) {
842                    putValue(PROP_SELECTED, e.getStateChange() == ItemEvent.SELECTED);
843                }
844    
845            }
846        }
847    
848        protected void handlePropertyChangeFrozen(boolean oldValue, boolean newValue) {
849            myEntriesTable.getSelectionModel().clearSelection();
850            myEntriesTable.setEnabled(!newValue);
851            theirEntriesTable.getSelectionModel().clearSelection();
852            theirEntriesTable.setEnabled(!newValue);
853            mergedEntriesTable.getSelectionModel().clearSelection();
854            mergedEntriesTable.setEnabled(!newValue);
855            freezeAction.putValue(FreezeActionProperties.PROP_SELECTED, newValue);
856            if (newValue) {
857                lblFrozenState.setText(
858                        tr("<html>Click <strong>{0}</strong> to start merging my and their entries.</html>",
859                                freezeAction.getValue(Action.NAME))
860                );
861            } else {
862                lblFrozenState.setText(
863                        tr("<html>Click <strong>{0}</strong> to finish merging my and their entries.</html>",
864                                freezeAction.getValue(Action.NAME))
865                );
866            }
867        }
868    
869        public void propertyChange(PropertyChangeEvent evt) {
870            if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
871                handlePropertyChangeFrozen((Boolean)evt.getOldValue(), (Boolean)evt.getNewValue());
872            }
873        }
874    
875        public ListMergeModel<T> getModel() {
876            return model;
877        }
878    
879        public void update(Observable o, Object arg) {
880            lblMyVersion.setText(
881                    trn("My version ({0} entry)", "My version ({0} entries)", model.getMyEntriesSize(), model.getMyEntriesSize())
882            );
883            lblMergedVersion.setText(
884                    trn("Merged version ({0} entry)", "Merged version ({0} entries)", model.getMergedEntriesSize(), model.getMergedEntriesSize())
885            );
886            lblTheirVersion.setText(
887                    trn("Their version ({0} entry)", "Their version ({0} entries)", model.getTheirEntriesSize(), model.getTheirEntriesSize())
888            );
889        }
890        
891        public void unlinkAsListener() {
892            myEntriesTable.unlinkAsListener();
893            mergedEntriesTable.unlinkAsListener();
894            theirEntriesTable.unlinkAsListener();
895        }
896    
897        /**
898         * Synchronizes scrollbar adjustments between a set of
899         * {@link Adjustable}s. Whenever the adjustment of one of
900         * the registerd Adjustables is updated the adjustment of
901         * the other registered Adjustables is adjusted too.
902         *
903         */
904        class AdjustmentSynchronizer implements AdjustmentListener {
905    
906            private final  ArrayList<Adjustable> synchronizedAdjustables;
907            private final  HashMap<Adjustable, Boolean> enabledMap;
908    
909            private final Observable observable;
910    
911            public AdjustmentSynchronizer() {
912                synchronizedAdjustables = new ArrayList<Adjustable>();
913                enabledMap = new HashMap<Adjustable, Boolean>();
914                observable = new Observable();
915            }
916    
917            /**
918             * registers an {@link Adjustable} for participation in synchronized
919             * scrolling.
920             *
921             * @param adjustable the adjustable
922             */
923            public void participateInSynchronizedScrolling(Adjustable adjustable) {
924                if (adjustable == null)
925                    return;
926                if (synchronizedAdjustables.contains(adjustable))
927                    return;
928                synchronizedAdjustables.add(adjustable);
929                setParticipatingInSynchronizedScrolling(adjustable, true);
930                adjustable.addAdjustmentListener(this);
931            }
932    
933            /**
934             * event handler for {@link AdjustmentEvent}s
935             *
936             */
937            public void adjustmentValueChanged(AdjustmentEvent e) {
938                if (! enabledMap.get(e.getAdjustable()))
939                    return;
940                for (Adjustable a : synchronizedAdjustables) {
941                    if (a != e.getAdjustable() && isParticipatingInSynchronizedScrolling(a)) {
942                        a.setValue(e.getValue());
943                    }
944                }
945            }
946    
947            /**
948             * sets whether adjustable participates in adjustment synchronization
949             * or not
950             *
951             * @param adjustable the adjustable
952             */
953            protected void setParticipatingInSynchronizedScrolling(Adjustable adjustable, boolean isParticipating) {
954                CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable");
955                if (! synchronizedAdjustables.contains(adjustable))
956                    throw new IllegalStateException(tr("Adjustable {0} not registered yet. Cannot set participation in synchronized adjustment.", adjustable));
957    
958                enabledMap.put(adjustable, isParticipating);
959                observable.notifyObservers();
960            }
961    
962            /**
963             * returns true if an adjustable is participating in synchronized scrolling
964             *
965             * @param adjustable the adjustable
966             * @return true, if the adjustable is participating in synchronized scrolling, false otherwise
967             * @throws IllegalStateException thrown, if adjustable is not registered for synchronized scrolling
968             */
969            protected boolean isParticipatingInSynchronizedScrolling(Adjustable adjustable) throws IllegalStateException {
970                if (! synchronizedAdjustables.contains(adjustable))
971                    throw new IllegalStateException(tr("Adjustable {0} not registered yet.", adjustable));
972    
973                return enabledMap.get(adjustable);
974            }
975    
976            /**
977             * wires a {@link JCheckBox} to  the adjustment synchronizer, in such a way  that:
978             * <li>
979             *   <ol>state changes in the checkbox control whether the adjustable participates
980             *      in synchronized adjustment</ol>
981             *   <ol>state changes in this {@link AdjustmentSynchronizer} are reflected in the
982             *      {@link JCheckBox}</ol>
983             * </li>
984             *
985             *
986             * @param view  the checkbox to control whether an adjustable participates in synchronized
987             *      adjustment
988             * @param adjustable the adjustable
989             * @exception IllegalArgumentException thrown, if view is null
990             * @exception IllegalArgumentException thrown, if adjustable is null
991             */
992            protected void adapt(final JCheckBox view, final Adjustable adjustable) throws IllegalStateException {
993                CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable");
994                CheckParameterUtil.ensureParameterNotNull(view, "view");
995    
996                if (! synchronizedAdjustables.contains(adjustable)) {
997                    participateInSynchronizedScrolling(adjustable);
998                }
999    
1000                // register an item lister with the check box
1001                //
1002                view.addItemListener(new ItemListener() {
1003                    public void itemStateChanged(ItemEvent e) {
1004                        switch(e.getStateChange()) {
1005                        case ItemEvent.SELECTED:
1006                            if (!isParticipatingInSynchronizedScrolling(adjustable)) {
1007                                setParticipatingInSynchronizedScrolling(adjustable, true);
1008                            }
1009                            break;
1010                        case ItemEvent.DESELECTED:
1011                            if (isParticipatingInSynchronizedScrolling(adjustable)) {
1012                                setParticipatingInSynchronizedScrolling(adjustable, false);
1013                            }
1014                            break;
1015                        }
1016                    }
1017                });
1018    
1019                observable.addObserver(
1020                        new Observer() {
1021                            public void update(Observable o, Object arg) {
1022                                boolean sync = isParticipatingInSynchronizedScrolling(adjustable);
1023                                if (view.isSelected() != sync) {
1024                                    view.setSelected(sync);
1025                                }
1026                            }
1027                        }
1028                );
1029                setParticipatingInSynchronizedScrolling(adjustable, true);
1030                view.setSelected(true);
1031            }
1032        }
1033        
1034        protected final <P extends OsmPrimitive> OsmDataLayer findLayerFor(P primitive) {
1035            if (primitive != null) {
1036                List<OsmDataLayer> layers = Main.map.mapView.getLayersOfType(OsmDataLayer.class);
1037                // Find layer with same dataset
1038                for (OsmDataLayer layer : layers) {
1039                    if (layer.data == primitive.getDataSet()) {
1040                        return layer;
1041                    }
1042                }
1043                // Conflict after merging layers: a dataset could be no more in any layer, try to find another layer with same primitive
1044                for (OsmDataLayer layer : layers) {
1045                    final Collection<? extends OsmPrimitive> collection;
1046                    if (primitive instanceof Way) {
1047                        collection = layer.data.getWays();
1048                    } else if (primitive instanceof Relation) {
1049                        collection = layer.data.getRelations();
1050                    } else {
1051                        collection = layer.data.allPrimitives();
1052                    }
1053                    for (OsmPrimitive p : collection) {
1054                        if (p.getPrimitiveId().equals(primitive.getPrimitiveId())) {
1055                            return layer;
1056                        }
1057                    }
1058                }
1059            }
1060            return null;
1061        }
1062    }