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.event.ActionEvent;
007import java.text.DecimalFormat;
008import java.util.Arrays;
009import java.util.List;
010import java.util.stream.Collectors;
011
012import javax.swing.AbstractAction;
013import javax.swing.Action;
014import javax.swing.BorderFactory;
015import javax.swing.JButton;
016import javax.swing.JComponent;
017import javax.swing.JLabel;
018import javax.swing.JPanel;
019import javax.swing.event.ChangeEvent;
020import javax.swing.event.ChangeListener;
021
022import org.openstreetmap.josm.data.conflict.Conflict;
023import org.openstreetmap.josm.data.coor.LatLon;
024import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
025import org.openstreetmap.josm.data.osm.OsmPrimitive;
026import org.openstreetmap.josm.gui.conflict.ConflictColors;
027import org.openstreetmap.josm.gui.conflict.pair.AbstractMergePanel;
028import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
029import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
030import org.openstreetmap.josm.gui.history.VersionInfoPanel;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.ImageProvider;
033import org.openstreetmap.josm.tools.Utils;
034
035/**
036 * This class represents a UI component for resolving conflicts in some properties of {@link OsmPrimitive}.
037 * @since 1654
038 */
039public class PropertiesMerger extends AbstractMergePanel implements ChangeListener, IConflictResolver {
040    private static final DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000000");
041
042    private final JLabel lblMyCoordinates = buildValueLabel("label.mycoordinates");
043    private final JLabel lblMergedCoordinates = buildValueLabel("label.mergedcoordinates");
044    private final JLabel lblTheirCoordinates = buildValueLabel("label.theircoordinates");
045
046    private final JLabel lblMyDeletedState = buildValueLabel("label.mydeletedstate");
047    private final JLabel lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate");
048    private final JLabel lblTheirDeletedState = buildValueLabel("label.theirdeletedstate");
049
050    private final JLabel lblMyReferrers = buildValueLabel("label.myreferrers");
051    private final JLabel lblTheirReferrers = buildValueLabel("label.theirreferrers");
052
053    private final transient PropertiesMergeModel model = new PropertiesMergeModel();
054    private final VersionInfoPanel mineVersionInfo = new VersionInfoPanel();
055    private final VersionInfoPanel theirVersionInfo = new VersionInfoPanel();
056
057    /**
058     * Constructs a new {@code PropertiesMerger}.
059     */
060    public PropertiesMerger() {
061        model.addChangeListener(this);
062        buildRows();
063    }
064
065    @Override
066    protected List<? extends MergeRow> getRows() {
067        return Arrays.asList(
068                new AbstractMergePanel.TitleRow(),
069                new VersionInfoRow(),
070                new MergeCoordinatesRow(),
071                new UndecideCoordinatesRow(),
072                new MergeDeletedStateRow(),
073                new UndecideDeletedStateRow(),
074                new ReferrersRow(),
075                new EmptyFillRow());
076    }
077
078    protected static JLabel buildValueLabel(String name) {
079        JLabel lbl = new JLabel();
080        lbl.setName(name);
081        lbl.setHorizontalAlignment(JLabel.CENTER);
082        lbl.setOpaque(true);
083        lbl.setBorder(BorderFactory.createLoweredBevelBorder());
084        return lbl;
085    }
086
087    protected static String coordToString(LatLon coord) {
088        if (coord == null)
089            return tr("(none)");
090        StringBuilder sb = new StringBuilder();
091        sb.append('(')
092        .append(COORD_FORMATTER.format(coord.lat()))
093        .append(',')
094        .append(COORD_FORMATTER.format(coord.lon()))
095        .append(')');
096        return sb.toString();
097    }
098
099    protected static String deletedStateToString(Boolean deleted) {
100        if (deleted == null)
101            return tr("(none)");
102        if (deleted)
103            return tr("deleted");
104        else
105            return tr("not deleted");
106    }
107
108    protected static String referrersToString(List<OsmPrimitive> referrers) {
109        if (referrers.isEmpty())
110            return tr("(none)");
111        return referrers.stream()
112                .map(r -> Utils.escapeReservedCharactersHTML(r.getDisplayName(DefaultNameFormatter.getInstance())) + "<br>")
113                .collect(Collectors.joining("", "<html>", "</html>"));
114    }
115
116    protected void updateCoordinates() {
117        lblMyCoordinates.setText(coordToString(model.getMyCoords()));
118        lblMergedCoordinates.setText(coordToString(model.getMergedCoords()));
119        lblTheirCoordinates.setText(coordToString(model.getTheirCoords()));
120        if (!model.hasCoordConflict()) {
121            lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
122            lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
123            lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
124        } else {
125            if (!model.isDecidedCoord()) {
126                lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
127                lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
128                lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
129            } else {
130                lblMyCoordinates.setBackground(
131                        model.isCoordMergeDecision(MergeDecisionType.KEEP_MINE)
132                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
133                );
134                lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_DECIDED.get());
135                lblTheirCoordinates.setBackground(
136                        model.isCoordMergeDecision(MergeDecisionType.KEEP_THEIR)
137                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
138                );
139            }
140        }
141    }
142
143    protected void updateDeletedState() {
144        lblMyDeletedState.setText(deletedStateToString(model.getMyDeletedState()));
145        lblMergedDeletedState.setText(deletedStateToString(model.getMergedDeletedState()));
146        lblTheirDeletedState.setText(deletedStateToString(model.getTheirDeletedState()));
147
148        if (!model.hasDeletedStateConflict()) {
149            lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
150            lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
151            lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
152        } else {
153            if (!model.isDecidedDeletedState()) {
154                lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
155                lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
156                lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
157            } else {
158                lblMyDeletedState.setBackground(
159                        model.isDeletedStateDecision(MergeDecisionType.KEEP_MINE)
160                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
161                );
162                lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_DECIDED.get());
163                lblTheirDeletedState.setBackground(
164                        model.isDeletedStateDecision(MergeDecisionType.KEEP_THEIR)
165                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
166                );
167            }
168        }
169    }
170
171    protected void updateReferrers() {
172        lblMyReferrers.setText(referrersToString(model.getMyReferrers()));
173        lblMyReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
174        lblTheirReferrers.setText(referrersToString(model.getTheirReferrers()));
175        lblTheirReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
176    }
177
178    @Override
179    public void stateChanged(ChangeEvent e) {
180        updateCoordinates();
181        updateDeletedState();
182        updateReferrers();
183    }
184
185    /**
186     * Returns properties merge model.
187     * @return properties merge model
188     */
189    public PropertiesMergeModel getModel() {
190        return model;
191    }
192
193    private final class MergeDeletedStateRow extends AbstractMergePanel.MergeRow {
194        @Override
195        protected JComponent rowTitle() {
196            return new JLabel(tr("Deleted State:"));
197        }
198
199        @Override
200        protected JComponent mineField() {
201            return lblMyDeletedState;
202        }
203
204        @Override
205        protected JComponent mineButton() {
206            KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction();
207            model.addChangeListener(actKeepMyDeletedState);
208            JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState);
209            btnKeepMyDeletedState.setName("button.keepmydeletedstate");
210            return btnKeepMyDeletedState;
211        }
212
213        @Override
214        protected JComponent merged() {
215            return lblMergedDeletedState;
216        }
217
218        @Override
219        protected JComponent theirsButton() {
220            KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction();
221            model.addChangeListener(actKeepTheirDeletedState);
222            JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState);
223            btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate");
224            return btnKeepTheirDeletedState;
225        }
226
227        @Override
228        protected JComponent theirsField() {
229            return lblTheirDeletedState;
230        }
231    }
232
233    private final class MergeCoordinatesRow extends AbstractMergePanel.MergeRow {
234        @Override
235        protected JComponent rowTitle() {
236            return new JLabel(tr("Coordinates:"));
237        }
238
239        @Override
240        protected JComponent mineField() {
241            return lblMyCoordinates;
242        }
243
244        @Override
245        protected JComponent mineButton() {
246            KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction();
247            model.addChangeListener(actKeepMyCoordinates);
248            JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates);
249            btnKeepMyCoordinates.setName("button.keepmycoordinates");
250            return btnKeepMyCoordinates;
251        }
252
253        @Override
254        protected JComponent merged() {
255            return lblMergedCoordinates;
256        }
257
258        @Override
259        protected JComponent theirsButton() {
260            KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction();
261            model.addChangeListener(actKeepTheirCoordinates);
262            JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates);
263            btnKeepTheirCoordinates.setName("button.keeptheircoordinates");
264            return btnKeepTheirCoordinates;
265        }
266
267        @Override
268        protected JComponent theirsField() {
269            return lblTheirCoordinates;
270        }
271    }
272
273    private final class UndecideCoordinatesRow extends AbstractUndecideRow {
274        @Override
275        protected UndecideCoordinateConflictAction createAction() {
276            UndecideCoordinateConflictAction action = new UndecideCoordinateConflictAction();
277            model.addChangeListener(action);
278            return action;
279        }
280
281        @Override
282        protected String getButtonName() {
283            return "button.undecidecoordinates";
284        }
285    }
286
287    private final class UndecideDeletedStateRow extends AbstractUndecideRow {
288        @Override
289        protected UndecideDeletedStateConflictAction createAction() {
290            UndecideDeletedStateConflictAction action = new UndecideDeletedStateConflictAction();
291            model.addChangeListener(action);
292            return action;
293        }
294
295        @Override
296        protected String getButtonName() {
297            return "button.undecidedeletedstate";
298        }
299    }
300
301    private final class VersionInfoRow extends AbstractMergePanel.MergeRowWithoutButton {
302        @Override
303        protected JComponent mineField() {
304            return mineVersionInfo;
305        }
306
307        @Override
308        protected JComponent theirsField() {
309            return theirVersionInfo;
310        }
311    }
312
313    private final class ReferrersRow extends AbstractMergePanel.MergeRow {
314        @Override
315        protected JComponent rowTitle() {
316            return new JLabel(tr("Referenced by:"));
317        }
318
319        @Override
320        protected JComponent mineField() {
321            return lblMyReferrers;
322        }
323
324        @Override
325        protected JComponent theirsField() {
326            return lblTheirReferrers;
327        }
328    }
329
330    private static final class EmptyFillRow extends AbstractMergePanel.MergeRow {
331        @Override
332        protected JComponent merged() {
333            return new JPanel();
334        }
335
336        @Override
337        protected void addConstraints(GBC constraints, int columnIndex) {
338            super.addConstraints(constraints, columnIndex);
339            // fill to bottom
340            constraints.weighty = 1;
341        }
342    }
343
344    class KeepMyCoordinatesAction extends AbstractAction implements ChangeListener {
345        KeepMyCoordinatesAction() {
346            new ImageProvider("dialogs/conflict", "tagkeepmine").getResource().attachImageIcon(this, true);
347            putValue(Action.SHORT_DESCRIPTION, tr("Keep my coordinates"));
348        }
349
350        @Override
351        public void actionPerformed(ActionEvent e) {
352            model.decideCoordsConflict(MergeDecisionType.KEEP_MINE);
353        }
354
355        @Override
356        public void stateChanged(ChangeEvent e) {
357            setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getMyCoords() != null);
358        }
359    }
360
361    class KeepTheirCoordinatesAction extends AbstractAction implements ChangeListener {
362        KeepTheirCoordinatesAction() {
363            new ImageProvider("dialogs/conflict", "tagkeeptheir").getResource().attachImageIcon(this, true);
364            putValue(Action.SHORT_DESCRIPTION, tr("Keep their coordinates"));
365        }
366
367        @Override
368        public void actionPerformed(ActionEvent e) {
369            model.decideCoordsConflict(MergeDecisionType.KEEP_THEIR);
370        }
371
372        @Override
373        public void stateChanged(ChangeEvent e) {
374            setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getTheirCoords() != null);
375        }
376    }
377
378    class UndecideCoordinateConflictAction extends AbstractAction implements ChangeListener {
379        UndecideCoordinateConflictAction() {
380            new ImageProvider("dialogs/conflict", "tagundecide").getResource().attachImageIcon(this, true);
381            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between different coordinates"));
382        }
383
384        @Override
385        public void actionPerformed(ActionEvent e) {
386            model.decideCoordsConflict(MergeDecisionType.UNDECIDED);
387        }
388
389        @Override
390        public void stateChanged(ChangeEvent e) {
391            setEnabled(model.hasCoordConflict() && model.isDecidedCoord());
392        }
393    }
394
395    class KeepMyDeletedStateAction extends AbstractAction implements ChangeListener {
396        KeepMyDeletedStateAction() {
397            new ImageProvider("dialogs/conflict", "tagkeepmine").getResource().attachImageIcon(this, true);
398            putValue(Action.SHORT_DESCRIPTION, tr("Keep my deleted state"));
399        }
400
401        @Override
402        public void actionPerformed(ActionEvent e) {
403            model.decideDeletedStateConflict(MergeDecisionType.KEEP_MINE);
404        }
405
406        @Override
407        public void stateChanged(ChangeEvent e) {
408            setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState());
409        }
410    }
411
412    class KeepTheirDeletedStateAction extends AbstractAction implements ChangeListener {
413        KeepTheirDeletedStateAction() {
414            new ImageProvider("dialogs/conflict", "tagkeeptheir").getResource().attachImageIcon(this, true);
415            putValue(Action.SHORT_DESCRIPTION, tr("Keep their deleted state"));
416        }
417
418        @Override
419        public void actionPerformed(ActionEvent e) {
420            model.decideDeletedStateConflict(MergeDecisionType.KEEP_THEIR);
421        }
422
423        @Override
424        public void stateChanged(ChangeEvent e) {
425            setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState());
426        }
427    }
428
429    class UndecideDeletedStateConflictAction extends AbstractAction implements ChangeListener {
430        UndecideDeletedStateConflictAction() {
431            new ImageProvider("dialogs/conflict", "tagundecide").getResource().attachImageIcon(this, true);
432            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between deleted state"));
433        }
434
435        @Override
436        public void actionPerformed(ActionEvent e) {
437            model.decideDeletedStateConflict(MergeDecisionType.UNDECIDED);
438        }
439
440        @Override
441        public void stateChanged(ChangeEvent e) {
442            setEnabled(model.hasDeletedStateConflict() && model.isDecidedDeletedState());
443        }
444    }
445
446    @Override
447    public void deletePrimitive(boolean deleted) {
448        if (deleted) {
449            if (model.getMergedCoords() == null) {
450                model.decideCoordsConflict(MergeDecisionType.KEEP_MINE);
451            }
452        } else {
453            model.decideCoordsConflict(MergeDecisionType.UNDECIDED);
454        }
455    }
456
457    @Override
458    public void populate(Conflict<? extends OsmPrimitive> conflict) {
459        model.populate(conflict);
460        mineVersionInfo.update(conflict.getMy(), true);
461        theirVersionInfo.update(conflict.getTheir(), false);
462    }
463
464    @Override
465    public void decideRemaining(MergeDecisionType decision) {
466        if (!model.isDecidedDeletedState()) {
467            model.decideDeletedStateConflict(decision);
468        }
469        if (!model.isDecidedCoord()) {
470            model.decideCoordsConflict(decision);
471        }
472    }
473}