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