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