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 of {@link OsmPrimitive}.
033 * @since 1654
034 */
035public class PropertiesMerger extends JPanel implements Observer, IConflictResolver {
036    private static final DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000000");
037
038    private JLabel lblMyCoordinates;
039    private JLabel lblMergedCoordinates;
040    private JLabel lblTheirCoordinates;
041
042    private JLabel lblMyDeletedState;
043    private JLabel lblMergedDeletedState;
044    private JLabel lblTheirDeletedState;
045
046    private JLabel lblMyReferrers;
047    private JLabel lblTheirReferrers;
048
049    private final transient PropertiesMergeModel model;
050
051    /**
052     * Constructs a new {@code PropertiesMerger}.
053     */
054    public PropertiesMerger() {
055        model = new PropertiesMergeModel();
056        model.addObserver(this);
057        build();
058    }
059
060    protected JLabel buildValueLabel(String name) {
061        JLabel lbl = new JLabel();
062        lbl.setName(name);
063        lbl.setHorizontalAlignment(JLabel.CENTER);
064        lbl.setOpaque(true);
065        lbl.setBorder(BorderFactory.createLoweredBevelBorder());
066        return lbl;
067    }
068
069    protected void buildHeaderRow() {
070        GridBagConstraints gc = new GridBagConstraints();
071
072        gc.gridx = 1;
073        gc.gridy = 0;
074        gc.gridwidth = 1;
075        gc.gridheight = 1;
076        gc.fill = GridBagConstraints.NONE;
077        gc.anchor = GridBagConstraints.CENTER;
078        gc.weightx = 0.0;
079        gc.weighty = 0.0;
080        gc.insets = new Insets(10, 0, 10, 0);
081        JLabel lblMyVersion = new JLabel(tr("My version"));
082        lblMyVersion.setToolTipText(tr("Properties in my dataset, i.e. the local dataset"));
083        add(lblMyVersion, gc);
084
085        gc.gridx = 3;
086        gc.gridy = 0;
087        JLabel lblMergedVersion = new JLabel(tr("Merged version"));
088        lblMergedVersion.setToolTipText(
089                tr("Properties in the merged element. They will replace properties in my elements when merge decisions are applied."));
090        add(lblMergedVersion, gc);
091
092        gc.gridx = 5;
093        gc.gridy = 0;
094        JLabel lblTheirVersion = new JLabel(tr("Their version"));
095        lblTheirVersion.setToolTipText(tr("Properties in their dataset, i.e. the server dataset"));
096        add(lblTheirVersion, gc);
097    }
098
099    protected void buildCoordinateConflictRows() {
100        GridBagConstraints gc = new GridBagConstraints();
101
102        gc.gridx = 0;
103        gc.gridy = 1;
104        gc.gridwidth = 1;
105        gc.gridheight = 1;
106        gc.fill = GridBagConstraints.HORIZONTAL;
107        gc.anchor = GridBagConstraints.LINE_START;
108        gc.weightx = 0.0;
109        gc.weighty = 0.0;
110        gc.insets = new Insets(0, 5, 0, 5);
111        add(new JLabel(tr("Coordinates:")), gc);
112
113        gc.gridx = 1;
114        gc.gridy = 1;
115        gc.fill = GridBagConstraints.BOTH;
116        gc.anchor = GridBagConstraints.CENTER;
117        gc.weightx = 0.33;
118        gc.weighty = 0.0;
119        add(lblMyCoordinates = buildValueLabel("label.mycoordinates"), gc);
120
121        gc.gridx = 2;
122        gc.gridy = 1;
123        gc.fill = GridBagConstraints.NONE;
124        gc.anchor = GridBagConstraints.CENTER;
125        gc.weightx = 0.0;
126        gc.weighty = 0.0;
127        KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction();
128        model.addObserver(actKeepMyCoordinates);
129        JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates);
130        btnKeepMyCoordinates.setName("button.keepmycoordinates");
131        add(btnKeepMyCoordinates, gc);
132
133        gc.gridx = 3;
134        gc.gridy = 1;
135        gc.fill = GridBagConstraints.BOTH;
136        gc.anchor = GridBagConstraints.CENTER;
137        gc.weightx = 0.33;
138        gc.weighty = 0.0;
139        add(lblMergedCoordinates = buildValueLabel("label.mergedcoordinates"), gc);
140
141        gc.gridx = 4;
142        gc.gridy = 1;
143        gc.fill = GridBagConstraints.NONE;
144        gc.anchor = GridBagConstraints.CENTER;
145        gc.weightx = 0.0;
146        gc.weighty = 0.0;
147        KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction();
148        model.addObserver(actKeepTheirCoordinates);
149        JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates);
150        add(btnKeepTheirCoordinates, gc);
151
152        gc.gridx = 5;
153        gc.gridy = 1;
154        gc.fill = GridBagConstraints.BOTH;
155        gc.anchor = GridBagConstraints.CENTER;
156        gc.weightx = 0.33;
157        gc.weighty = 0.0;
158        add(lblTheirCoordinates = buildValueLabel("label.theircoordinates"), gc);
159
160        // ---------------------------------------------------
161        gc.gridx = 3;
162        gc.gridy = 2;
163        gc.fill = GridBagConstraints.NONE;
164        gc.anchor = GridBagConstraints.CENTER;
165        gc.weightx = 0.0;
166        gc.weighty = 0.0;
167        UndecideCoordinateConflictAction actUndecideCoordinates = new UndecideCoordinateConflictAction();
168        model.addObserver(actUndecideCoordinates);
169        JButton btnUndecideCoordinates = new JButton(actUndecideCoordinates);
170        add(btnUndecideCoordinates, gc);
171    }
172
173    protected void buildDeletedStateConflictRows() {
174        GridBagConstraints gc = new GridBagConstraints();
175
176        gc.gridx = 0;
177        gc.gridy = 3;
178        gc.gridwidth = 1;
179        gc.gridheight = 1;
180        gc.fill = GridBagConstraints.BOTH;
181        gc.anchor = GridBagConstraints.LINE_START;
182        gc.weightx = 0.0;
183        gc.weighty = 0.0;
184        gc.insets = new Insets(0, 5, 0, 5);
185        add(new JLabel(tr("Deleted State:")), gc);
186
187        gc.gridx = 1;
188        gc.gridy = 3;
189        gc.fill = GridBagConstraints.BOTH;
190        gc.anchor = GridBagConstraints.CENTER;
191        gc.weightx = 0.33;
192        gc.weighty = 0.0;
193        add(lblMyDeletedState = buildValueLabel("label.mydeletedstate"), gc);
194
195        gc.gridx = 2;
196        gc.gridy = 3;
197        gc.fill = GridBagConstraints.NONE;
198        gc.anchor = GridBagConstraints.CENTER;
199        gc.weightx = 0.0;
200        gc.weighty = 0.0;
201        KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction();
202        model.addObserver(actKeepMyDeletedState);
203        JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState);
204        btnKeepMyDeletedState.setName("button.keepmydeletedstate");
205        add(btnKeepMyDeletedState, gc);
206
207        gc.gridx = 3;
208        gc.gridy = 3;
209        gc.fill = GridBagConstraints.BOTH;
210        gc.anchor = GridBagConstraints.CENTER;
211        gc.weightx = 0.33;
212        gc.weighty = 0.0;
213        add(lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate"), gc);
214
215        gc.gridx = 4;
216        gc.gridy = 3;
217        gc.fill = GridBagConstraints.NONE;
218        gc.anchor = GridBagConstraints.CENTER;
219        gc.weightx = 0.0;
220        gc.weighty = 0.0;
221        KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction();
222        model.addObserver(actKeepTheirDeletedState);
223        JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState);
224        btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate");
225        add(btnKeepTheirDeletedState, gc);
226
227        gc.gridx = 5;
228        gc.gridy = 3;
229        gc.fill = GridBagConstraints.BOTH;
230        gc.anchor = GridBagConstraints.CENTER;
231        gc.weightx = 0.33;
232        gc.weighty = 0.0;
233        add(lblTheirDeletedState = buildValueLabel("label.theirdeletedstate"), gc);
234
235        // ---------------------------------------------------
236        gc.gridx = 3;
237        gc.gridy = 4;
238        gc.fill = GridBagConstraints.NONE;
239        gc.anchor = GridBagConstraints.CENTER;
240        gc.weightx = 0.0;
241        gc.weighty = 0.0;
242        UndecideDeletedStateConflictAction actUndecideDeletedState = new UndecideDeletedStateConflictAction();
243        model.addObserver(actUndecideDeletedState);
244        JButton btnUndecideDeletedState = new JButton(actUndecideDeletedState);
245        btnUndecideDeletedState.setName("button.undecidedeletedstate");
246        add(btnUndecideDeletedState, gc);
247    }
248
249    protected void buildReferrersRow() {
250        GridBagConstraints gc = new GridBagConstraints();
251
252        gc.gridx = 0;
253        gc.gridy = 7;
254        gc.gridwidth = 1;
255        gc.gridheight = 1;
256        gc.fill = GridBagConstraints.BOTH;
257        gc.anchor = GridBagConstraints.LINE_START;
258        gc.weightx = 0.0;
259        gc.weighty = 0.0;
260        gc.insets = new Insets(0, 5, 0, 5);
261        add(new JLabel(tr("Referenced by:")), gc);
262
263        gc.gridx = 1;
264        gc.gridy = 7;
265        gc.fill = GridBagConstraints.BOTH;
266        gc.anchor = GridBagConstraints.CENTER;
267        gc.weightx = 0.33;
268        gc.weighty = 0.0;
269        add(lblMyReferrers = buildValueLabel("label.myreferrers"), gc);
270
271        gc.gridx = 5;
272        gc.gridy = 7;
273        gc.fill = GridBagConstraints.BOTH;
274        gc.anchor = GridBagConstraints.CENTER;
275        gc.weightx = 0.33;
276        gc.weighty = 0.0;
277        add(lblTheirReferrers = buildValueLabel("label.theirreferrers"), gc);
278    }
279
280    protected final void build() {
281        setLayout(new GridBagLayout());
282        buildHeaderRow();
283        buildCoordinateConflictRows();
284        buildDeletedStateConflictRows();
285        buildReferrersRow();
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        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() && model.getMyCoords() != null);
407        }
408    }
409
410    class KeepTheirCoordinatesAction extends AbstractAction implements Observer {
411        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() && model.getTheirCoords() != null);
424        }
425    }
426
427    class UndecideCoordinateConflictAction extends AbstractAction implements Observer {
428        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        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        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        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}