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