001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.pair.properties;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.Insets;
009import java.awt.event.ActionEvent;
010import java.text.DecimalFormat;
011import java.util.List;
012import java.util.Observable;
013import java.util.Observer;
014
015import javax.swing.AbstractAction;
016import javax.swing.Action;
017import javax.swing.BorderFactory;
018import javax.swing.JButton;
019import javax.swing.JLabel;
020import javax.swing.JPanel;
021
022import org.openstreetmap.josm.data.conflict.Conflict;
023import org.openstreetmap.josm.data.coor.LatLon;
024import org.openstreetmap.josm.data.osm.OsmPrimitive;
025import org.openstreetmap.josm.gui.DefaultNameFormatter;
026import org.openstreetmap.josm.gui.conflict.ConflictColors;
027import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
028import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
029import org.openstreetmap.josm.tools.ImageProvider;
030
031/**
032 * This class represents a UI component for resolving conflicts in some properties
033 * of {@link OsmPrimitive}.
034 *
035 */
036public class PropertiesMerger extends JPanel implements Observer, IConflictResolver {
037    private static final DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000000");
038
039    private JLabel lblMyCoordinates;
040    private JLabel lblMergedCoordinates;
041    private JLabel lblTheirCoordinates;
042
043    private JLabel lblMyDeletedState;
044    private JLabel lblMergedDeletedState;
045    private JLabel lblTheirDeletedState;
046
047    private JLabel lblMyReferrers;
048    private JLabel lblTheirReferrers;
049
050    private final PropertiesMergeModel model;
051
052    protected JLabel buildValueLabel(String name) {
053        JLabel lbl = new JLabel();
054        lbl.setName(name);
055        lbl.setHorizontalAlignment(JLabel.CENTER);
056        lbl.setOpaque(true);
057        lbl.setBorder(BorderFactory.createLoweredBevelBorder());
058        return lbl;
059    }
060
061    protected void buildHeaderRow() {
062        GridBagConstraints gc = new GridBagConstraints();
063
064        gc.gridx = 1;
065        gc.gridy = 0;
066        gc.gridwidth = 1;
067        gc.gridheight = 1;
068        gc.fill = GridBagConstraints.NONE;
069        gc.anchor = GridBagConstraints.CENTER;
070        gc.weightx = 0.0;
071        gc.weighty = 0.0;
072        gc.insets = new Insets(10,0,10,0);
073        JLabel lblMyVersion = new JLabel(tr("My version"));
074        lblMyVersion.setToolTipText(tr("Properties in my dataset, i.e. the local dataset"));
075        add(lblMyVersion, gc);
076
077        gc.gridx = 3;
078        gc.gridy = 0;
079        JLabel lblMergedVersion = new JLabel(tr("Merged version"));
080        lblMergedVersion.setToolTipText(tr("Properties in the merged element. They will replace properties in my elements when merge decisions are applied."));
081        add(lblMergedVersion, gc);
082
083        gc.gridx = 5;
084        gc.gridy = 0;
085        JLabel lblTheirVersion = new JLabel(tr("Their version"));
086        lblTheirVersion.setToolTipText(tr("Properties in their dataset, i.e. the server dataset"));
087        add(lblTheirVersion, gc);
088    }
089
090    protected void buildCoordinateConflictRows() {
091        GridBagConstraints gc = new GridBagConstraints();
092
093        gc.gridx = 0;
094        gc.gridy = 1;
095        gc.gridwidth = 1;
096        gc.gridheight = 1;
097        gc.fill = GridBagConstraints.HORIZONTAL;
098        gc.anchor = GridBagConstraints.LINE_START;
099        gc.weightx = 0.0;
100        gc.weighty = 0.0;
101        gc.insets = new Insets(0,5,0,5);
102        add(new JLabel(tr("Coordinates:")), gc);
103
104        gc.gridx = 1;
105        gc.gridy = 1;
106        gc.fill = GridBagConstraints.BOTH;
107        gc.anchor = GridBagConstraints.CENTER;
108        gc.weightx = 0.33;
109        gc.weighty = 0.0;
110        add(lblMyCoordinates = buildValueLabel("label.mycoordinates"), gc);
111
112        gc.gridx = 2;
113        gc.gridy = 1;
114        gc.fill = GridBagConstraints.NONE;
115        gc.anchor = GridBagConstraints.CENTER;
116        gc.weightx = 0.0;
117        gc.weighty = 0.0;
118        KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction();
119        model.addObserver(actKeepMyCoordinates);
120        JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates);
121        btnKeepMyCoordinates.setName("button.keepmycoordinates");
122        add(btnKeepMyCoordinates, gc);
123
124        gc.gridx = 3;
125        gc.gridy = 1;
126        gc.fill = GridBagConstraints.BOTH;
127        gc.anchor = GridBagConstraints.CENTER;
128        gc.weightx = 0.33;
129        gc.weighty = 0.0;
130        add(lblMergedCoordinates = buildValueLabel("label.mergedcoordinates"), gc);
131
132        gc.gridx = 4;
133        gc.gridy = 1;
134        gc.fill = GridBagConstraints.NONE;
135        gc.anchor = GridBagConstraints.CENTER;
136        gc.weightx = 0.0;
137        gc.weighty = 0.0;
138        KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction();
139        model.addObserver(actKeepTheirCoordinates);
140        JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates);
141        add(btnKeepTheirCoordinates, gc);
142
143        gc.gridx = 5;
144        gc.gridy = 1;
145        gc.fill = GridBagConstraints.BOTH;
146        gc.anchor = GridBagConstraints.CENTER;
147        gc.weightx = 0.33;
148        gc.weighty = 0.0;
149        add(lblTheirCoordinates = buildValueLabel("label.theircoordinates"), gc);
150
151        // ---------------------------------------------------
152        gc.gridx = 3;
153        gc.gridy = 2;
154        gc.fill = GridBagConstraints.NONE;
155        gc.anchor = GridBagConstraints.CENTER;
156        gc.weightx = 0.0;
157        gc.weighty = 0.0;
158        UndecideCoordinateConflictAction actUndecideCoordinates = new UndecideCoordinateConflictAction();
159        model.addObserver(actUndecideCoordinates);
160        JButton btnUndecideCoordinates = new JButton(actUndecideCoordinates);
161        add(btnUndecideCoordinates, gc);
162    }
163
164    protected void buildDeletedStateConflictRows() {
165        GridBagConstraints gc = new GridBagConstraints();
166
167        gc.gridx = 0;
168        gc.gridy = 3;
169        gc.gridwidth = 1;
170        gc.gridheight = 1;
171        gc.fill = GridBagConstraints.BOTH;
172        gc.anchor = GridBagConstraints.LINE_START;
173        gc.weightx = 0.0;
174        gc.weighty = 0.0;
175        gc.insets = new Insets(0,5,0,5);
176        add(new JLabel(tr("Deleted State:")), gc);
177
178        gc.gridx = 1;
179        gc.gridy = 3;
180        gc.fill = GridBagConstraints.BOTH;
181        gc.anchor = GridBagConstraints.CENTER;
182        gc.weightx = 0.33;
183        gc.weighty = 0.0;
184        add(lblMyDeletedState = buildValueLabel("label.mydeletedstate"), gc);
185
186        gc.gridx = 2;
187        gc.gridy = 3;
188        gc.fill = GridBagConstraints.NONE;
189        gc.anchor = GridBagConstraints.CENTER;
190        gc.weightx = 0.0;
191        gc.weighty = 0.0;
192        KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction();
193        model.addObserver(actKeepMyDeletedState);
194        JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState);
195        btnKeepMyDeletedState.setName("button.keepmydeletedstate");
196        add(btnKeepMyDeletedState, gc);
197
198        gc.gridx = 3;
199        gc.gridy = 3;
200        gc.fill = GridBagConstraints.BOTH;
201        gc.anchor = GridBagConstraints.CENTER;
202        gc.weightx = 0.33;
203        gc.weighty = 0.0;
204        add(lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate"), gc);
205
206        gc.gridx = 4;
207        gc.gridy = 3;
208        gc.fill = GridBagConstraints.NONE;
209        gc.anchor = GridBagConstraints.CENTER;
210        gc.weightx = 0.0;
211        gc.weighty = 0.0;
212        KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction();
213        model.addObserver(actKeepTheirDeletedState);
214        JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState);
215        btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate");
216        add(btnKeepTheirDeletedState, gc);
217
218        gc.gridx = 5;
219        gc.gridy = 3;
220        gc.fill = GridBagConstraints.BOTH;
221        gc.anchor = GridBagConstraints.CENTER;
222        gc.weightx = 0.33;
223        gc.weighty = 0.0;
224        add(lblTheirDeletedState = buildValueLabel("label.theirdeletedstate"), gc);
225
226        // ---------------------------------------------------
227        gc.gridx = 3;
228        gc.gridy = 4;
229        gc.fill = GridBagConstraints.NONE;
230        gc.anchor = GridBagConstraints.CENTER;
231        gc.weightx = 0.0;
232        gc.weighty = 0.0;
233        UndecideDeletedStateConflictAction actUndecideDeletedState = new UndecideDeletedStateConflictAction();
234        model.addObserver(actUndecideDeletedState);
235        JButton btnUndecideDeletedState = new JButton(actUndecideDeletedState);
236        btnUndecideDeletedState.setName("button.undecidedeletedstate");
237        add(btnUndecideDeletedState, gc);
238    }
239
240    protected void buildReferrersRow() {
241        GridBagConstraints gc = new GridBagConstraints();
242
243        gc.gridx = 0;
244        gc.gridy = 7;
245        gc.gridwidth = 1;
246        gc.gridheight = 1;
247        gc.fill = GridBagConstraints.BOTH;
248        gc.anchor = GridBagConstraints.LINE_START;
249        gc.weightx = 0.0;
250        gc.weighty = 0.0;
251        gc.insets = new Insets(0,5,0,5);
252        add(new JLabel(tr("Referenced by:")), gc);
253
254        gc.gridx = 1;
255        gc.gridy = 7;
256        gc.fill = GridBagConstraints.BOTH;
257        gc.anchor = GridBagConstraints.CENTER;
258        gc.weightx = 0.33;
259        gc.weighty = 0.0;
260        add(lblMyReferrers = buildValueLabel("label.myreferrers"), gc);
261
262        gc.gridx = 5;
263        gc.gridy = 7;
264        gc.fill = GridBagConstraints.BOTH;
265        gc.anchor = GridBagConstraints.CENTER;
266        gc.weightx = 0.33;
267        gc.weighty = 0.0;
268        add(lblTheirReferrers = buildValueLabel("label.theirreferrers"), gc);
269    }
270
271    protected final void build() {
272        setLayout(new GridBagLayout());
273        buildHeaderRow();
274        buildCoordinateConflictRows();
275        buildDeletedStateConflictRows();
276        buildReferrersRow();
277    }
278
279    /**
280     * Constructs a new {@code PropertiesMerger}.
281     */
282    public PropertiesMerger() {
283        model = new PropertiesMergeModel();
284        model.addObserver(this);
285        build();
286    }
287
288    public String coordToString(LatLon coord) {
289        if (coord == null)
290            return tr("(none)");
291        StringBuilder sb = new StringBuilder();
292        sb.append("(")
293        .append(COORD_FORMATTER.format(coord.lat()))
294        .append(",")
295        .append(COORD_FORMATTER.format(coord.lon()))
296        .append(")");
297        return sb.toString();
298    }
299
300    public String deletedStateToString(Boolean deleted) {
301        if (deleted == null)
302            return tr("(none)");
303        if (deleted)
304            return tr("deleted");
305        else
306            return tr("not deleted");
307    }
308
309    public String referrersToString(List<OsmPrimitive> referrers) {
310        if (referrers.isEmpty())
311            return tr("(none)");
312        StringBuilder str = new StringBuilder("<html>");
313        for (OsmPrimitive r: referrers) {
314            str.append(r.getDisplayName(DefaultNameFormatter.getInstance())).append("<br>");
315        }
316        str.append("</html>");
317        return str.toString();
318    }
319
320    protected void updateCoordinates() {
321        lblMyCoordinates.setText(coordToString(model.getMyCoords()));
322        lblMergedCoordinates.setText(coordToString(model.getMergedCoords()));
323        lblTheirCoordinates.setText(coordToString(model.getTheirCoords()));
324        if (! model.hasCoordConflict()) {
325            lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
326            lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
327            lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
328        } else {
329            if (!model.isDecidedCoord()) {
330                lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
331                lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
332                lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
333            } else {
334                lblMyCoordinates.setBackground(
335                        model.isCoordMergeDecision(MergeDecisionType.KEEP_MINE)
336                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
337                );
338                lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_DECIDED.get());
339                lblTheirCoordinates.setBackground(
340                        model.isCoordMergeDecision(MergeDecisionType.KEEP_THEIR)
341                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
342                );
343            }
344        }
345    }
346
347    protected void updateDeletedState() {
348        lblMyDeletedState.setText(deletedStateToString(model.getMyDeletedState()));
349        lblMergedDeletedState.setText(deletedStateToString(model.getMergedDeletedState()));
350        lblTheirDeletedState.setText(deletedStateToString(model.getTheirDeletedState()));
351
352        if (! model.hasDeletedStateConflict()) {
353            lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
354            lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
355            lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
356        } else {
357            if (!model.isDecidedDeletedState()) {
358                lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
359                lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
360                lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
361            } else {
362                lblMyDeletedState.setBackground(
363                        model.isDeletedStateDecision(MergeDecisionType.KEEP_MINE)
364                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
365                );
366                lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_DECIDED.get());
367                lblTheirDeletedState.setBackground(
368                        model.isDeletedStateDecision(MergeDecisionType.KEEP_THEIR)
369                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
370                );
371            }
372        }
373    }
374
375    protected void updateReferrers() {
376        lblMyReferrers.setText(referrersToString(model.getMyReferrers()));
377        lblMyReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
378        lblTheirReferrers.setText(referrersToString(model.getTheirReferrers()));
379        lblTheirReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
380    }
381
382    @Override
383    public void update(Observable o, Object arg) {
384        updateCoordinates();
385        updateDeletedState();
386        updateReferrers();
387    }
388
389    public PropertiesMergeModel getModel() {
390        return model;
391    }
392
393    class KeepMyCoordinatesAction extends AbstractAction implements Observer {
394        public KeepMyCoordinatesAction() {
395            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine"));
396            putValue(Action.SHORT_DESCRIPTION, tr("Keep my coordinates"));
397        }
398
399        @Override
400        public void actionPerformed(ActionEvent e) {
401            model.decideCoordsConflict(MergeDecisionType.KEEP_MINE);
402        }
403
404        @Override
405        public void update(Observable o, Object arg) {
406            setEnabled(model.hasCoordConflict() && ! model.isDecidedCoord());
407        }
408    }
409
410    class KeepTheirCoordinatesAction extends AbstractAction implements Observer {
411        public KeepTheirCoordinatesAction() {
412            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir"));
413            putValue(Action.SHORT_DESCRIPTION, tr("Keep their coordinates"));
414        }
415
416        @Override
417        public void actionPerformed(ActionEvent e) {
418            model.decideCoordsConflict(MergeDecisionType.KEEP_THEIR);
419        }
420
421        @Override
422        public void update(Observable o, Object arg) {
423            setEnabled(model.hasCoordConflict() && ! model.isDecidedCoord());
424        }
425    }
426
427    class UndecideCoordinateConflictAction extends AbstractAction implements Observer {
428        public UndecideCoordinateConflictAction() {
429            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide"));
430            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between different coordinates"));
431        }
432
433        @Override
434        public void actionPerformed(ActionEvent e) {
435            model.decideCoordsConflict(MergeDecisionType.UNDECIDED);
436        }
437
438        @Override
439        public void update(Observable o, Object arg) {
440            setEnabled(model.hasCoordConflict() && model.isDecidedCoord());
441        }
442    }
443
444    class KeepMyDeletedStateAction extends AbstractAction implements Observer {
445        public KeepMyDeletedStateAction() {
446            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine"));
447            putValue(Action.SHORT_DESCRIPTION, tr("Keep my deleted state"));
448        }
449
450        @Override
451        public void actionPerformed(ActionEvent e) {
452            model.decideDeletedStateConflict(MergeDecisionType.KEEP_MINE);
453        }
454
455        @Override
456        public void update(Observable o, Object arg) {
457            setEnabled(model.hasDeletedStateConflict() && ! model.isDecidedDeletedState());
458        }
459    }
460
461    class KeepTheirDeletedStateAction extends AbstractAction implements Observer {
462        public KeepTheirDeletedStateAction() {
463            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir"));
464            putValue(Action.SHORT_DESCRIPTION, tr("Keep their deleted state"));
465        }
466
467        @Override
468        public void actionPerformed(ActionEvent e) {
469            model.decideDeletedStateConflict(MergeDecisionType.KEEP_THEIR);
470        }
471
472        @Override
473        public void update(Observable o, Object arg) {
474            setEnabled(model.hasDeletedStateConflict() && ! model.isDecidedDeletedState());
475        }
476    }
477
478    class UndecideDeletedStateConflictAction extends AbstractAction implements Observer {
479        public UndecideDeletedStateConflictAction() {
480            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide"));
481            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between deleted state"));
482        }
483
484        @Override
485        public void actionPerformed(ActionEvent e) {
486            model.decideDeletedStateConflict(MergeDecisionType.UNDECIDED);
487        }
488
489        @Override
490        public void update(Observable o, Object arg) {
491            setEnabled(model.hasDeletedStateConflict() && model.isDecidedDeletedState());
492        }
493    }
494
495    @Override
496    public void deletePrimitive(boolean deleted) {
497        if (deleted) {
498            if (model.getMergedCoords() == null) {
499                model.decideCoordsConflict(MergeDecisionType.KEEP_MINE);
500            }
501        } else {
502            model.decideCoordsConflict(MergeDecisionType.UNDECIDED);
503        }
504    }
505
506    @Override
507    public void populate(Conflict<? extends OsmPrimitive> conflict) {
508        model.populate(conflict);
509    }
510}