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