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