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