source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/pair/properties/PropertiesMerger.java @ 11848

Last change on this file since 11848 was 11848, checked in by Don-vip, 2 years ago

fix #14613 - Special HTML characters not escaped in GUI error messages

  • Property svn:eol-style set to native
File size: 20.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.pair.properties;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GridBagConstraints;
7import java.awt.GridBagLayout;
8import java.awt.Insets;
9import java.awt.event.ActionEvent;
10import java.text.DecimalFormat;
11import java.util.List;
12
13import javax.swing.AbstractAction;
14import javax.swing.Action;
15import javax.swing.BorderFactory;
16import javax.swing.JButton;
17import javax.swing.JLabel;
18import javax.swing.JPanel;
19import javax.swing.event.ChangeEvent;
20import javax.swing.event.ChangeListener;
21
22import org.openstreetmap.josm.data.conflict.Conflict;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.gui.DefaultNameFormatter;
26import org.openstreetmap.josm.gui.conflict.ConflictColors;
27import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver;
28import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
29import org.openstreetmap.josm.gui.history.VersionInfoPanel;
30import org.openstreetmap.josm.tools.ImageProvider;
31import org.openstreetmap.josm.tools.Utils;
32
33/**
34 * This class represents a UI component for resolving conflicts in some properties of {@link OsmPrimitive}.
35 * @since 1654
36 */
37public class PropertiesMerger extends JPanel implements ChangeListener, IConflictResolver {
38    private static final DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000000");
39
40    private final JLabel lblMyCoordinates = buildValueLabel("label.mycoordinates");
41    private final JLabel lblMergedCoordinates = buildValueLabel("label.mergedcoordinates");
42    private final JLabel lblTheirCoordinates = buildValueLabel("label.theircoordinates");
43
44    private final JLabel lblMyDeletedState = buildValueLabel("label.mydeletedstate");
45    private final JLabel lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate");
46    private final JLabel lblTheirDeletedState = buildValueLabel("label.theirdeletedstate");
47
48    private final JLabel lblMyReferrers = buildValueLabel("label.myreferrers");
49    private final JLabel lblTheirReferrers = buildValueLabel("label.theirreferrers");
50
51    private final transient PropertiesMergeModel model;
52    private final VersionInfoPanel mineVersionInfo = new VersionInfoPanel();
53    private final VersionInfoPanel theirVersionInfo = new VersionInfoPanel();
54
55    /**
56     * Constructs a new {@code PropertiesMerger}.
57     */
58    public PropertiesMerger() {
59        model = new PropertiesMergeModel();
60        model.addChangeListener(this);
61        build();
62    }
63
64    protected static JLabel buildValueLabel(String name) {
65        JLabel lbl = new JLabel();
66        lbl.setName(name);
67        lbl.setHorizontalAlignment(JLabel.CENTER);
68        lbl.setOpaque(true);
69        lbl.setBorder(BorderFactory.createLoweredBevelBorder());
70        return lbl;
71    }
72
73    protected void buildHeaderRow() {
74        GridBagConstraints gc = new GridBagConstraints();
75
76        gc.gridx = 1;
77        gc.gridy = 0;
78        gc.gridwidth = 1;
79        gc.gridheight = 1;
80        gc.fill = GridBagConstraints.NONE;
81        gc.anchor = GridBagConstraints.CENTER;
82        gc.weightx = 0.0;
83        gc.weighty = 0.0;
84        gc.insets = new Insets(10, 0, 0, 0);
85        JLabel lblMyVersion = new JLabel(tr("My version"));
86        lblMyVersion.setToolTipText(tr("Properties in my dataset, i.e. the local dataset"));
87        lblMyVersion.setLabelFor(mineVersionInfo);
88        add(lblMyVersion, gc);
89
90        gc.gridx = 3;
91        JLabel lblMergedVersion = new JLabel(tr("Merged version"));
92        lblMergedVersion.setToolTipText(
93                tr("Properties in the merged element. They will replace properties in my elements when merge decisions are applied."));
94        add(lblMergedVersion, gc);
95
96        gc.gridx = 5;
97        JLabel lblTheirVersion = new JLabel(tr("Their version"));
98        lblTheirVersion.setToolTipText(tr("Properties in their dataset, i.e. the server dataset"));
99        lblMyVersion.setLabelFor(theirVersionInfo);
100        add(lblTheirVersion, gc);
101
102        gc.gridx = 1;
103        gc.gridy = 1;
104        gc.fill = GridBagConstraints.HORIZONTAL;
105        gc.anchor = GridBagConstraints.LINE_START;
106        gc.insets = new Insets(0, 0, 20, 0);
107        add(mineVersionInfo, gc);
108
109        gc.gridx = 5;
110        add(theirVersionInfo, gc);
111    }
112
113    protected void buildCoordinateConflictRows() {
114        GridBagConstraints gc = new GridBagConstraints();
115
116        gc.gridx = 0;
117        gc.gridy = 2;
118        gc.gridwidth = 1;
119        gc.gridheight = 1;
120        gc.fill = GridBagConstraints.HORIZONTAL;
121        gc.anchor = GridBagConstraints.LINE_START;
122        gc.weightx = 0.0;
123        gc.weighty = 0.0;
124        gc.insets = new Insets(0, 5, 0, 5);
125        add(new JLabel(tr("Coordinates:")), gc);
126
127        gc.gridx = 1;
128        gc.fill = GridBagConstraints.BOTH;
129        gc.anchor = GridBagConstraints.CENTER;
130        gc.weightx = 0.33;
131        gc.weighty = 0.0;
132        add(lblMyCoordinates, gc);
133
134        gc.gridx = 2;
135        gc.fill = GridBagConstraints.NONE;
136        gc.anchor = GridBagConstraints.CENTER;
137        gc.weightx = 0.0;
138        gc.weighty = 0.0;
139        KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction();
140        model.addChangeListener(actKeepMyCoordinates);
141        JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates);
142        btnKeepMyCoordinates.setName("button.keepmycoordinates");
143        add(btnKeepMyCoordinates, gc);
144
145        gc.gridx = 3;
146        gc.fill = GridBagConstraints.BOTH;
147        gc.anchor = GridBagConstraints.CENTER;
148        gc.weightx = 0.33;
149        gc.weighty = 0.0;
150        add(lblMergedCoordinates, gc);
151
152        gc.gridx = 4;
153        gc.fill = GridBagConstraints.NONE;
154        gc.anchor = GridBagConstraints.CENTER;
155        gc.weightx = 0.0;
156        gc.weighty = 0.0;
157        KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction();
158        model.addChangeListener(actKeepTheirCoordinates);
159        JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates);
160        add(btnKeepTheirCoordinates, gc);
161
162        gc.gridx = 5;
163        gc.fill = GridBagConstraints.BOTH;
164        gc.anchor = GridBagConstraints.CENTER;
165        gc.weightx = 0.33;
166        gc.weighty = 0.0;
167        add(lblTheirCoordinates, gc);
168
169        // ---------------------------------------------------
170        gc.gridx = 3;
171        gc.gridy = 3;
172        gc.fill = GridBagConstraints.NONE;
173        gc.anchor = GridBagConstraints.CENTER;
174        gc.weightx = 0.0;
175        gc.weighty = 0.0;
176        UndecideCoordinateConflictAction actUndecideCoordinates = new UndecideCoordinateConflictAction();
177        model.addChangeListener(actUndecideCoordinates);
178        JButton btnUndecideCoordinates = new JButton(actUndecideCoordinates);
179        add(btnUndecideCoordinates, gc);
180    }
181
182    protected void buildDeletedStateConflictRows() {
183        GridBagConstraints gc = new GridBagConstraints();
184
185        gc.gridx = 0;
186        gc.gridy = 4;
187        gc.gridwidth = 1;
188        gc.gridheight = 1;
189        gc.fill = GridBagConstraints.BOTH;
190        gc.anchor = GridBagConstraints.LINE_START;
191        gc.weightx = 0.0;
192        gc.weighty = 0.0;
193        gc.insets = new Insets(0, 5, 0, 5);
194        add(new JLabel(tr("Deleted State:")), gc);
195
196        gc.gridx = 1;
197        gc.fill = GridBagConstraints.BOTH;
198        gc.anchor = GridBagConstraints.CENTER;
199        gc.weightx = 0.33;
200        gc.weighty = 0.0;
201        add(lblMyDeletedState, gc);
202
203        gc.gridx = 2;
204        gc.fill = GridBagConstraints.NONE;
205        gc.anchor = GridBagConstraints.CENTER;
206        gc.weightx = 0.0;
207        gc.weighty = 0.0;
208        KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction();
209        model.addChangeListener(actKeepMyDeletedState);
210        JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState);
211        btnKeepMyDeletedState.setName("button.keepmydeletedstate");
212        add(btnKeepMyDeletedState, gc);
213
214        gc.gridx = 3;
215        gc.fill = GridBagConstraints.BOTH;
216        gc.anchor = GridBagConstraints.CENTER;
217        gc.weightx = 0.33;
218        gc.weighty = 0.0;
219        add(lblMergedDeletedState, gc);
220
221        gc.gridx = 4;
222        gc.fill = GridBagConstraints.NONE;
223        gc.anchor = GridBagConstraints.CENTER;
224        gc.weightx = 0.0;
225        gc.weighty = 0.0;
226        KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction();
227        model.addChangeListener(actKeepTheirDeletedState);
228        JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState);
229        btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate");
230        add(btnKeepTheirDeletedState, gc);
231
232        gc.gridx = 5;
233        gc.fill = GridBagConstraints.BOTH;
234        gc.anchor = GridBagConstraints.CENTER;
235        gc.weightx = 0.33;
236        gc.weighty = 0.0;
237        add(lblTheirDeletedState, gc);
238
239        // ---------------------------------------------------
240        gc.gridx = 3;
241        gc.gridy = 5;
242        gc.fill = GridBagConstraints.NONE;
243        gc.anchor = GridBagConstraints.CENTER;
244        gc.weightx = 0.0;
245        gc.weighty = 0.0;
246        UndecideDeletedStateConflictAction actUndecideDeletedState = new UndecideDeletedStateConflictAction();
247        model.addChangeListener(actUndecideDeletedState);
248        JButton btnUndecideDeletedState = new JButton(actUndecideDeletedState);
249        btnUndecideDeletedState.setName("button.undecidedeletedstate");
250        add(btnUndecideDeletedState, gc);
251    }
252
253    protected void buildReferrersRow() {
254        GridBagConstraints gc = new GridBagConstraints();
255
256        gc.gridx = 0;
257        gc.gridy = 7;
258        gc.gridwidth = 1;
259        gc.gridheight = 1;
260        gc.fill = GridBagConstraints.BOTH;
261        gc.anchor = GridBagConstraints.LINE_START;
262        gc.weightx = 0.0;
263        gc.weighty = 0.0;
264        gc.insets = new Insets(0, 5, 0, 5);
265        add(new JLabel(tr("Referenced by:")), gc);
266
267        gc.gridx = 1;
268        gc.gridy = 7;
269        gc.fill = GridBagConstraints.BOTH;
270        gc.anchor = GridBagConstraints.CENTER;
271        gc.weightx = 0.33;
272        gc.weighty = 0.0;
273        add(lblMyReferrers, gc);
274
275        gc.gridx = 5;
276        gc.gridy = 7;
277        gc.fill = GridBagConstraints.BOTH;
278        gc.anchor = GridBagConstraints.CENTER;
279        gc.weightx = 0.33;
280        gc.weighty = 0.0;
281        add(lblTheirReferrers, gc);
282    }
283
284    protected final void build() {
285        setLayout(new GridBagLayout());
286        buildHeaderRow();
287        buildCoordinateConflictRows();
288        buildDeletedStateConflictRows();
289        buildReferrersRow();
290    }
291
292    protected static String coordToString(LatLon coord) {
293        if (coord == null)
294            return tr("(none)");
295        StringBuilder sb = new StringBuilder();
296        sb.append('(')
297        .append(COORD_FORMATTER.format(coord.lat()))
298        .append(',')
299        .append(COORD_FORMATTER.format(coord.lon()))
300        .append(')');
301        return sb.toString();
302    }
303
304    protected static String deletedStateToString(Boolean deleted) {
305        if (deleted == null)
306            return tr("(none)");
307        if (deleted)
308            return tr("deleted");
309        else
310            return tr("not deleted");
311    }
312
313    protected static String referrersToString(List<OsmPrimitive> referrers) {
314        if (referrers.isEmpty())
315            return tr("(none)");
316        StringBuilder str = new StringBuilder("<html>");
317        for (OsmPrimitive r: referrers) {
318            str.append(Utils.escapeReservedCharactersHTML(r.getDisplayName(DefaultNameFormatter.getInstance()))).append("<br>");
319        }
320        str.append("</html>");
321        return str.toString();
322    }
323
324    protected void updateCoordinates() {
325        lblMyCoordinates.setText(coordToString(model.getMyCoords()));
326        lblMergedCoordinates.setText(coordToString(model.getMergedCoords()));
327        lblTheirCoordinates.setText(coordToString(model.getTheirCoords()));
328        if (!model.hasCoordConflict()) {
329            lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
330            lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
331            lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
332        } else {
333            if (!model.isDecidedCoord()) {
334                lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
335                lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
336                lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
337            } else {
338                lblMyCoordinates.setBackground(
339                        model.isCoordMergeDecision(MergeDecisionType.KEEP_MINE)
340                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
341                );
342                lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_DECIDED.get());
343                lblTheirCoordinates.setBackground(
344                        model.isCoordMergeDecision(MergeDecisionType.KEEP_THEIR)
345                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
346                );
347            }
348        }
349    }
350
351    protected void updateDeletedState() {
352        lblMyDeletedState.setText(deletedStateToString(model.getMyDeletedState()));
353        lblMergedDeletedState.setText(deletedStateToString(model.getMergedDeletedState()));
354        lblTheirDeletedState.setText(deletedStateToString(model.getTheirDeletedState()));
355
356        if (!model.hasDeletedStateConflict()) {
357            lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
358            lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
359            lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
360        } else {
361            if (!model.isDecidedDeletedState()) {
362                lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
363                lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
364                lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get());
365            } else {
366                lblMyDeletedState.setBackground(
367                        model.isDeletedStateDecision(MergeDecisionType.KEEP_MINE)
368                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
369                );
370                lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_DECIDED.get());
371                lblTheirDeletedState.setBackground(
372                        model.isDeletedStateDecision(MergeDecisionType.KEEP_THEIR)
373                        ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get()
374                );
375            }
376        }
377    }
378
379    protected void updateReferrers() {
380        lblMyReferrers.setText(referrersToString(model.getMyReferrers()));
381        lblMyReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
382        lblTheirReferrers.setText(referrersToString(model.getTheirReferrers()));
383        lblTheirReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get());
384    }
385
386    @Override
387    public void stateChanged(ChangeEvent e) {
388        updateCoordinates();
389        updateDeletedState();
390        updateReferrers();
391    }
392
393    /**
394     * Returns properties merge model.
395     * @return properties merge model
396     */
397    public PropertiesMergeModel getModel() {
398        return model;
399    }
400
401    class KeepMyCoordinatesAction extends AbstractAction implements ChangeListener {
402        KeepMyCoordinatesAction() {
403            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine"));
404            putValue(Action.SHORT_DESCRIPTION, tr("Keep my coordinates"));
405        }
406
407        @Override
408        public void actionPerformed(ActionEvent e) {
409            model.decideCoordsConflict(MergeDecisionType.KEEP_MINE);
410        }
411
412        @Override
413        public void stateChanged(ChangeEvent e) {
414            setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getMyCoords() != null);
415        }
416    }
417
418    class KeepTheirCoordinatesAction extends AbstractAction implements ChangeListener {
419        KeepTheirCoordinatesAction() {
420            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir"));
421            putValue(Action.SHORT_DESCRIPTION, tr("Keep their coordinates"));
422        }
423
424        @Override
425        public void actionPerformed(ActionEvent e) {
426            model.decideCoordsConflict(MergeDecisionType.KEEP_THEIR);
427        }
428
429        @Override
430        public void stateChanged(ChangeEvent e) {
431            setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getTheirCoords() != null);
432        }
433    }
434
435    class UndecideCoordinateConflictAction extends AbstractAction implements ChangeListener {
436        UndecideCoordinateConflictAction() {
437            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide"));
438            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between different coordinates"));
439        }
440
441        @Override
442        public void actionPerformed(ActionEvent e) {
443            model.decideCoordsConflict(MergeDecisionType.UNDECIDED);
444        }
445
446        @Override
447        public void stateChanged(ChangeEvent e) {
448            setEnabled(model.hasCoordConflict() && model.isDecidedCoord());
449        }
450    }
451
452    class KeepMyDeletedStateAction extends AbstractAction implements ChangeListener {
453        KeepMyDeletedStateAction() {
454            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine"));
455            putValue(Action.SHORT_DESCRIPTION, tr("Keep my deleted state"));
456        }
457
458        @Override
459        public void actionPerformed(ActionEvent e) {
460            model.decideDeletedStateConflict(MergeDecisionType.KEEP_MINE);
461        }
462
463        @Override
464        public void stateChanged(ChangeEvent e) {
465            setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState());
466        }
467    }
468
469    class KeepTheirDeletedStateAction extends AbstractAction implements ChangeListener {
470        KeepTheirDeletedStateAction() {
471            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir"));
472            putValue(Action.SHORT_DESCRIPTION, tr("Keep their deleted state"));
473        }
474
475        @Override
476        public void actionPerformed(ActionEvent e) {
477            model.decideDeletedStateConflict(MergeDecisionType.KEEP_THEIR);
478        }
479
480        @Override
481        public void stateChanged(ChangeEvent e) {
482            setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState());
483        }
484    }
485
486    class UndecideDeletedStateConflictAction extends AbstractAction implements ChangeListener {
487        UndecideDeletedStateConflictAction() {
488            putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide"));
489            putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between deleted state"));
490        }
491
492        @Override
493        public void actionPerformed(ActionEvent e) {
494            model.decideDeletedStateConflict(MergeDecisionType.UNDECIDED);
495        }
496
497        @Override
498        public void stateChanged(ChangeEvent e) {
499            setEnabled(model.hasDeletedStateConflict() && model.isDecidedDeletedState());
500        }
501    }
502
503    @Override
504    public void deletePrimitive(boolean deleted) {
505        if (deleted) {
506            if (model.getMergedCoords() == null) {
507                model.decideCoordsConflict(MergeDecisionType.KEEP_MINE);
508            }
509        } else {
510            model.decideCoordsConflict(MergeDecisionType.UNDECIDED);
511        }
512    }
513
514    @Override
515    public void populate(Conflict<? extends OsmPrimitive> conflict) {
516        model.populate(conflict);
517        mineVersionInfo.update(conflict.getMy(), true);
518        theirVersionInfo.update(conflict.getTheir(), false);
519    }
520
521    @Override
522    public void decideRemaining(MergeDecisionType decision) {
523        if (!model.isDecidedCoord()) {
524            model.decideDeletedStateConflict(decision);
525        }
526        if (!model.isDecidedCoord()) {
527            model.decideCoordsConflict(decision);
528        }
529    }
530}
Note: See TracBrowser for help on using the repository browser.