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