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