001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.tags;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.BorderLayout;
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.Font;
012import java.awt.GridBagConstraints;
013import java.awt.GridBagLayout;
014import java.awt.Insets;
015import java.awt.event.ActionEvent;
016import java.beans.PropertyChangeEvent;
017import java.beans.PropertyChangeListener;
018import java.util.ArrayList;
019import java.util.EnumMap;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Map.Entry;
024
025import javax.swing.AbstractAction;
026import javax.swing.Action;
027import javax.swing.ImageIcon;
028import javax.swing.JDialog;
029import javax.swing.JLabel;
030import javax.swing.JPanel;
031import javax.swing.JTabbedPane;
032import javax.swing.JTable;
033import javax.swing.UIManager;
034import javax.swing.table.DefaultTableModel;
035import javax.swing.table.TableCellRenderer;
036
037import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
038import org.openstreetmap.josm.data.osm.TagCollection;
039import org.openstreetmap.josm.gui.SideButton;
040import org.openstreetmap.josm.gui.tagging.TagTableColumnModelBuilder;
041import org.openstreetmap.josm.gui.util.GuiHelper;
042import org.openstreetmap.josm.tools.ImageProvider;
043import org.openstreetmap.josm.tools.InputMapUtils;
044import org.openstreetmap.josm.tools.WindowGeometry;
045
046public class PasteTagsConflictResolverDialog extends JDialog implements PropertyChangeListener {
047    static final Map<OsmPrimitiveType, String> PANE_TITLES;
048    static {
049        PANE_TITLES = new EnumMap<>(OsmPrimitiveType.class);
050        PANE_TITLES.put(OsmPrimitiveType.NODE, tr("Tags from nodes"));
051        PANE_TITLES.put(OsmPrimitiveType.WAY, tr("Tags from ways"));
052        PANE_TITLES.put(OsmPrimitiveType.RELATION, tr("Tags from relations"));
053    }
054
055    enum Mode {
056        RESOLVING_ONE_TAGCOLLECTION_ONLY,
057        RESOLVING_TYPED_TAGCOLLECTIONS
058    }
059
060    private final TagConflictResolver allPrimitivesResolver = new TagConflictResolver();
061    private final transient Map<OsmPrimitiveType, TagConflictResolver> resolvers = new EnumMap<>(OsmPrimitiveType.class);
062    private final JTabbedPane tpResolvers = new JTabbedPane();
063    private Mode mode;
064    private boolean canceled;
065
066    private final ImageIcon iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved");
067    private final ImageIcon iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved");
068    private final StatisticsTableModel statisticsModel = new StatisticsTableModel();
069    private final JPanel pnlTagResolver = new JPanel(new BorderLayout());
070
071    /**
072     * Constructs a new {@code PasteTagsConflictResolverDialog}.
073     * @param owner parent component
074     */
075    public PasteTagsConflictResolverDialog(Component owner) {
076        super(GuiHelper.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL);
077        build();
078    }
079
080    protected final void build() {
081        setTitle(tr("Conflicts in pasted tags"));
082        for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
083            resolvers.put(type, new TagConflictResolver());
084            resolvers.get(type).getModel().addPropertyChangeListener(this);
085        }
086        getContentPane().setLayout(new GridBagLayout());
087        mode = null;
088        GridBagConstraints gc = new GridBagConstraints();
089        gc.gridx = 0;
090        gc.gridy = 0;
091        gc.fill = GridBagConstraints.HORIZONTAL;
092        gc.weightx = 1.0;
093        gc.weighty = 0.0;
094        getContentPane().add(buildSourceAndTargetInfoPanel(), gc);
095        gc.gridx = 0;
096        gc.gridy = 1;
097        gc.fill = GridBagConstraints.BOTH;
098        gc.weightx = 1.0;
099        gc.weighty = 1.0;
100        getContentPane().add(pnlTagResolver, gc);
101        gc.gridx = 0;
102        gc.gridy = 2;
103        gc.fill = GridBagConstraints.HORIZONTAL;
104        gc.weightx = 1.0;
105        gc.weighty = 0.0;
106        getContentPane().add(buildButtonPanel(), gc);
107        InputMapUtils.addEscapeAction(getRootPane(), new CancelAction());
108
109    }
110
111    protected JPanel buildButtonPanel() {
112        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
113
114        // -- apply button
115        ApplyAction applyAction = new ApplyAction();
116        allPrimitivesResolver.getModel().addPropertyChangeListener(applyAction);
117        for (TagConflictResolver r : resolvers.values()) {
118            r.getModel().addPropertyChangeListener(applyAction);
119        }
120        pnl.add(new SideButton(applyAction));
121
122        // -- cancel button
123        CancelAction cancelAction = new CancelAction();
124        pnl.add(new SideButton(cancelAction));
125
126        return pnl;
127    }
128
129    protected JPanel buildSourceAndTargetInfoPanel() {
130        JPanel pnl = new JPanel(new BorderLayout());
131        pnl.add(new StatisticsInfoTable(statisticsModel), BorderLayout.CENTER);
132        return pnl;
133    }
134
135    /**
136     * Initializes the conflict resolver for a specific type of primitives
137     *
138     * @param type the type of primitives
139     * @param tc the tags belonging to this type of primitives
140     * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target
141     */
142    protected void initResolver(OsmPrimitiveType type, TagCollection tc, Map<OsmPrimitiveType, Integer> targetStatistics) {
143        resolvers.get(type).getModel().populate(tc, tc.getKeysWithMultipleValues());
144        resolvers.get(type).getModel().prepareDefaultTagDecisions();
145        if (!tc.isEmpty() && targetStatistics.get(type) != null && targetStatistics.get(type) > 0) {
146            tpResolvers.add(PANE_TITLES.get(type), resolvers.get(type));
147        }
148    }
149
150    /**
151     * Populates the conflict resolver with one tag collection
152     *
153     * @param tagsForAllPrimitives  the tag collection
154     * @param sourceStatistics histogram of tag source, number of primitives of each type in the source
155     * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target
156     */
157    public void populate(TagCollection tagsForAllPrimitives, Map<OsmPrimitiveType, Integer> sourceStatistics,
158            Map<OsmPrimitiveType, Integer> targetStatistics) {
159        mode = Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY;
160        tagsForAllPrimitives = tagsForAllPrimitives == null ? new TagCollection() : tagsForAllPrimitives;
161        sourceStatistics = sourceStatistics == null ? new HashMap<>() : sourceStatistics;
162        targetStatistics = targetStatistics == null ? new HashMap<>() : targetStatistics;
163
164        // init the resolver
165        //
166        allPrimitivesResolver.getModel().populate(tagsForAllPrimitives, tagsForAllPrimitives.getKeysWithMultipleValues());
167        allPrimitivesResolver.getModel().prepareDefaultTagDecisions();
168
169        // prepare the dialog with one tag resolver
170        pnlTagResolver.removeAll();
171        pnlTagResolver.add(allPrimitivesResolver, BorderLayout.CENTER);
172
173        statisticsModel.reset();
174        StatisticsInfo info = new StatisticsInfo();
175        info.numTags = tagsForAllPrimitives.getKeys().size();
176        info.sourceInfo.putAll(sourceStatistics);
177        info.targetInfo.putAll(targetStatistics);
178        statisticsModel.append(info);
179        validate();
180    }
181
182    protected int getNumResolverTabs() {
183        return tpResolvers.getTabCount();
184    }
185
186    protected TagConflictResolver getResolver(int idx) {
187        return (TagConflictResolver) tpResolvers.getComponentAt(idx);
188    }
189
190    /**
191     * Populate the tag conflict resolver with tags for each type of primitives
192     *
193     * @param tagsForNodes the tags belonging to nodes in the paste source
194     * @param tagsForWays the tags belonging to way in the paste source
195     * @param tagsForRelations the tags belonging to relations in the paste source
196     * @param sourceStatistics histogram of tag source, number of primitives of each type in the source
197     * @param targetStatistics histogram of paste targets, number of primitives of each type in the paste target
198     */
199    public void populate(TagCollection tagsForNodes, TagCollection tagsForWays, TagCollection tagsForRelations,
200            Map<OsmPrimitiveType, Integer> sourceStatistics, Map<OsmPrimitiveType, Integer> targetStatistics) {
201        tagsForNodes = (tagsForNodes == null) ? new TagCollection() : tagsForNodes;
202        tagsForWays = (tagsForWays == null) ? new TagCollection() : tagsForWays;
203        tagsForRelations = (tagsForRelations == null) ? new TagCollection() : tagsForRelations;
204        if (tagsForNodes.isEmpty() && tagsForWays.isEmpty() && tagsForRelations.isEmpty()) {
205            populate(null, null, null);
206            return;
207        }
208        tpResolvers.removeAll();
209        initResolver(OsmPrimitiveType.NODE, tagsForNodes, targetStatistics);
210        initResolver(OsmPrimitiveType.WAY, tagsForWays, targetStatistics);
211        initResolver(OsmPrimitiveType.RELATION, tagsForRelations, targetStatistics);
212
213        pnlTagResolver.removeAll();
214        pnlTagResolver.add(tpResolvers, BorderLayout.CENTER);
215        mode = Mode.RESOLVING_TYPED_TAGCOLLECTIONS;
216        validate();
217        statisticsModel.reset();
218        if (!tagsForNodes.isEmpty()) {
219            StatisticsInfo info = new StatisticsInfo();
220            info.numTags = tagsForNodes.getKeys().size();
221            int numTargets = targetStatistics.get(OsmPrimitiveType.NODE) == null ? 0 : targetStatistics.get(OsmPrimitiveType.NODE);
222            if (numTargets > 0) {
223                info.sourceInfo.put(OsmPrimitiveType.NODE, sourceStatistics.get(OsmPrimitiveType.NODE));
224                info.targetInfo.put(OsmPrimitiveType.NODE, numTargets);
225                statisticsModel.append(info);
226            }
227        }
228        if (!tagsForWays.isEmpty()) {
229            StatisticsInfo info = new StatisticsInfo();
230            info.numTags = tagsForWays.getKeys().size();
231            int numTargets = targetStatistics.get(OsmPrimitiveType.WAY) == null ? 0 : targetStatistics.get(OsmPrimitiveType.WAY);
232            if (numTargets > 0) {
233                info.sourceInfo.put(OsmPrimitiveType.WAY, sourceStatistics.get(OsmPrimitiveType.WAY));
234                info.targetInfo.put(OsmPrimitiveType.WAY, numTargets);
235                statisticsModel.append(info);
236            }
237        }
238        if (!tagsForRelations.isEmpty()) {
239            StatisticsInfo info = new StatisticsInfo();
240            info.numTags = tagsForRelations.getKeys().size();
241            int numTargets = targetStatistics.get(OsmPrimitiveType.RELATION) == null ? 0 : targetStatistics.get(OsmPrimitiveType.RELATION);
242            if (numTargets > 0) {
243                info.sourceInfo.put(OsmPrimitiveType.RELATION, sourceStatistics.get(OsmPrimitiveType.RELATION));
244                info.targetInfo.put(OsmPrimitiveType.RELATION, numTargets);
245                statisticsModel.append(info);
246            }
247        }
248
249        for (int i = 0; i < getNumResolverTabs(); i++) {
250            if (!getResolver(i).getModel().isResolvedCompletely()) {
251                tpResolvers.setSelectedIndex(i);
252                break;
253            }
254        }
255    }
256
257    protected void setCanceled(boolean canceled) {
258        this.canceled = canceled;
259    }
260
261    public boolean isCanceled() {
262        return this.canceled;
263    }
264
265    final class CancelAction extends AbstractAction {
266
267        private CancelAction() {
268            putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
269            putValue(Action.NAME, tr("Cancel"));
270            new ImageProvider("cancel").getResource().attachImageIcon(this);
271            setEnabled(true);
272        }
273
274        @Override
275        public void actionPerformed(ActionEvent arg0) {
276            setVisible(false);
277            setCanceled(true);
278        }
279    }
280
281    final class ApplyAction extends AbstractAction implements PropertyChangeListener {
282
283        private ApplyAction() {
284            putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
285            putValue(Action.NAME, tr("Apply"));
286            new ImageProvider("ok").getResource().attachImageIcon(this);
287            updateEnabledState();
288        }
289
290        @Override
291        public void actionPerformed(ActionEvent arg0) {
292            setVisible(false);
293        }
294
295        void updateEnabledState() {
296            if (mode == null) {
297                setEnabled(false);
298            } else if (mode.equals(Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY)) {
299                setEnabled(allPrimitivesResolver.getModel().isResolvedCompletely());
300            } else {
301                setEnabled(resolvers.values().stream().allMatch(val -> val.getModel().isResolvedCompletely()));
302            }
303        }
304
305        @Override
306        public void propertyChange(PropertyChangeEvent evt) {
307            if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
308                updateEnabledState();
309            }
310        }
311    }
312
313    @Override
314    public void setVisible(boolean visible) {
315        if (visible) {
316            new WindowGeometry(
317                    getClass().getName() + ".geometry",
318                    WindowGeometry.centerOnScreen(new Dimension(600, 400))
319            ).applySafe(this);
320        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
321            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
322        }
323        super.setVisible(visible);
324    }
325
326    /**
327     * Returns conflict resolution.
328     * @return conflict resolution
329     */
330    public TagCollection getResolution() {
331        return allPrimitivesResolver.getModel().getResolution();
332    }
333
334    public TagCollection getResolution(OsmPrimitiveType type) {
335        if (type == null) return null;
336        return resolvers.get(type).getModel().getResolution();
337    }
338
339    @Override
340    public void propertyChange(PropertyChangeEvent evt) {
341        if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
342            TagConflictResolverModel model = (TagConflictResolverModel) evt.getSource();
343            for (int i = 0; i < tpResolvers.getTabCount(); i++) {
344                TagConflictResolver resolver = (TagConflictResolver) tpResolvers.getComponentAt(i);
345                if (model == resolver.getModel()) {
346                    tpResolvers.setIconAt(i,
347                            (Integer) evt.getNewValue() == 0 ? iconResolved : iconUnresolved
348                    );
349                }
350            }
351        }
352    }
353
354    static final class StatisticsInfo {
355        int numTags;
356        final Map<OsmPrimitiveType, Integer> sourceInfo;
357        final Map<OsmPrimitiveType, Integer> targetInfo;
358
359        StatisticsInfo() {
360            sourceInfo = new EnumMap<>(OsmPrimitiveType.class);
361            targetInfo = new EnumMap<>(OsmPrimitiveType.class);
362        }
363    }
364
365    static final class StatisticsTableModel extends DefaultTableModel {
366        private static final String[] HEADERS = new String[] {tr("Paste ..."), tr("From ..."), tr("To ...") };
367        private final transient List<StatisticsInfo> data = new ArrayList<>();
368
369        @Override
370        public Object getValueAt(int row, int column) {
371            if (row == 0)
372                return HEADERS[column];
373            else if (row -1 < data.size())
374                return data.get(row -1);
375            else
376                return null;
377        }
378
379        @Override
380        public boolean isCellEditable(int row, int column) {
381            return false;
382        }
383
384        @Override
385        public int getRowCount() {
386            return data == null ? 1 : data.size() + 1;
387        }
388
389        void reset() {
390            data.clear();
391        }
392
393        void append(StatisticsInfo info) {
394            data.add(info);
395            fireTableDataChanged();
396        }
397    }
398
399    static final class StatisticsInfoRenderer extends JLabel implements TableCellRenderer {
400        private void reset() {
401            setIcon(null);
402            setText("");
403            setFont(UIManager.getFont("Table.font"));
404        }
405
406        private void renderNumTags(StatisticsInfo info) {
407            if (info == null) return;
408            setText(trn("{0} tag", "{0} tags", info.numTags, info.numTags));
409        }
410
411        private void renderStatistics(Map<OsmPrimitiveType, Integer> stat) {
412            if (stat == null) return;
413            if (stat.isEmpty()) return;
414            if (stat.size() == 1) {
415                setIcon(ImageProvider.get(stat.keySet().iterator().next()));
416            } else {
417                setIcon(ImageProvider.get("data", "object"));
418            }
419            StringBuilder text = new StringBuilder();
420            for (Entry<OsmPrimitiveType, Integer> entry: stat.entrySet()) {
421                OsmPrimitiveType type = entry.getKey();
422                int numPrimitives = entry.getValue() == null ? 0 : entry.getValue();
423                if (numPrimitives == 0) {
424                    continue;
425                }
426                String msg;
427                switch(type) {
428                case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives, numPrimitives); break;
429                case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break;
430                case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break;
431                default: throw new AssertionError();
432                }
433                if (text.length() > 0) {
434                    text.append(", ");
435                }
436                text.append(msg);
437            }
438            setText(text.toString());
439        }
440
441        private void renderFrom(StatisticsInfo info) {
442            renderStatistics(info.sourceInfo);
443        }
444
445        private void renderTo(StatisticsInfo info) {
446            renderStatistics(info.targetInfo);
447        }
448
449        @Override
450        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
451                boolean hasFocus, int row, int column) {
452            reset();
453            if (value == null)
454                return this;
455
456            if (row == 0) {
457                setFont(getFont().deriveFont(Font.BOLD));
458                setText((String) value);
459            } else {
460                StatisticsInfo info = (StatisticsInfo) value;
461
462                switch(column) {
463                case 0: renderNumTags(info); break;
464                case 1: renderFrom(info); break;
465                case 2: renderTo(info); break;
466                default: // Do nothing
467                }
468            }
469            return this;
470        }
471    }
472
473    static final class StatisticsInfoTable extends JPanel {
474
475        StatisticsInfoTable(StatisticsTableModel model) {
476            JTable infoTable = new JTable(model,
477                    new TagTableColumnModelBuilder(new StatisticsInfoRenderer(), tr("Paste ..."), tr("From ..."), tr("To ...")).build());
478            infoTable.setShowHorizontalLines(true);
479            infoTable.setShowVerticalLines(false);
480            infoTable.setEnabled(false);
481            setLayout(new BorderLayout());
482            add(infoTable, BorderLayout.CENTER);
483        }
484
485        @Override
486        public Insets getInsets() {
487            Insets insets = super.getInsets();
488            insets.bottom = 20;
489            return insets;
490        }
491    }
492}