001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.conflict.tags;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trn;
006    
007    import java.awt.BorderLayout;
008    import java.awt.Component;
009    import java.awt.Dimension;
010    import java.awt.FlowLayout;
011    import java.awt.Font;
012    import java.awt.GridBagConstraints;
013    import java.awt.GridBagLayout;
014    import java.awt.Insets;
015    import java.awt.event.ActionEvent;
016    import java.beans.PropertyChangeEvent;
017    import java.beans.PropertyChangeListener;
018    import java.util.ArrayList;
019    import java.util.HashMap;
020    import java.util.List;
021    import java.util.Map;
022    
023    import javax.swing.AbstractAction;
024    import javax.swing.Action;
025    import javax.swing.ImageIcon;
026    import javax.swing.JDialog;
027    import javax.swing.JLabel;
028    import javax.swing.JOptionPane;
029    import javax.swing.JPanel;
030    import javax.swing.JTabbedPane;
031    import javax.swing.JTable;
032    import javax.swing.UIManager;
033    import javax.swing.table.DefaultTableColumnModel;
034    import javax.swing.table.DefaultTableModel;
035    import javax.swing.table.TableCellRenderer;
036    import javax.swing.table.TableColumn;
037    
038    import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
039    import org.openstreetmap.josm.data.osm.TagCollection;
040    import org.openstreetmap.josm.gui.SideButton;
041    import org.openstreetmap.josm.tools.ImageProvider;
042    import org.openstreetmap.josm.tools.WindowGeometry;
043    
044    public class PasteTagsConflictResolverDialog extends JDialog  implements PropertyChangeListener {
045        static private final Map<OsmPrimitiveType, String> PANE_TITLES;
046        static {
047            PANE_TITLES = new HashMap<OsmPrimitiveType, String>();
048            PANE_TITLES.put(OsmPrimitiveType.NODE, tr("Tags from nodes"));
049            PANE_TITLES.put(OsmPrimitiveType.WAY, tr("Tags from ways"));
050            PANE_TITLES.put(OsmPrimitiveType.RELATION, tr("Tags from relations"));
051        }
052    
053        private enum Mode {
054            RESOLVING_ONE_TAGCOLLECTION_ONLY,
055            RESOLVING_TYPED_TAGCOLLECTIONS
056        }
057    
058        private TagConflictResolver allPrimitivesResolver;
059        private Map<OsmPrimitiveType, TagConflictResolver> resolvers;
060        private JTabbedPane tpResolvers;
061        private Mode mode;
062        private boolean canceled = false;
063    
064        private ImageIcon iconResolved;
065        private ImageIcon iconUnresolved;
066        private StatisticsTableModel statisticsModel;
067        private JPanel pnlTagResolver;
068    
069        public PasteTagsConflictResolverDialog(Component owner) {
070            super(JOptionPane.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL);
071            build();
072            iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved");
073            iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved");
074        }
075    
076        protected void build() {
077            setTitle(tr("Conflicts in pasted tags"));
078            allPrimitivesResolver = new TagConflictResolver();
079            resolvers = new HashMap<OsmPrimitiveType, TagConflictResolver>();
080            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
081                resolvers.put(type, new TagConflictResolver());
082                resolvers.get(type).getModel().addPropertyChangeListener(this);
083            }
084            tpResolvers = new JTabbedPane();
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 = new JPanel(), 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();
110            pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
111    
112            // -- apply button
113            ApplyAction applyAction = new ApplyAction();
114            allPrimitivesResolver.getModel().addPropertyChangeListener(applyAction);
115            for (OsmPrimitiveType type: resolvers.keySet()) {
116                resolvers.get(type).getModel().addPropertyChangeListener(applyAction);
117            }
118            pnl.add(new SideButton(applyAction));
119    
120            // -- cancel button
121            CancelAction cancelAction = new CancelAction();
122            pnl.add(new SideButton(cancelAction));
123    
124            return pnl;
125        }
126    
127        protected JPanel buildSourceAndTargetInfoPanel() {
128            JPanel pnl = new JPanel();
129            pnl.setLayout(new BorderLayout());
130            statisticsModel = new StatisticsTableModel();
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, Map<OsmPrimitiveType,Integer> targetStatistics) {
158            mode = Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY;
159            tagsForAllPrimitives = tagsForAllPrimitives == null? new TagCollection() : tagsForAllPrimitives;
160            sourceStatistics = sourceStatistics == null ? new HashMap<OsmPrimitiveType, Integer>() :sourceStatistics;
161            targetStatistics = targetStatistics == null ? new HashMap<OsmPrimitiveType, Integer>() : targetStatistics;
162    
163            // init the resolver
164            //
165            allPrimitivesResolver.getModel().populate(tagsForAllPrimitives,tagsForAllPrimitives.getKeysWithMultipleValues());
166            allPrimitivesResolver.getModel().prepareDefaultTagDecisions();
167    
168            // prepare the dialog with one tag resolver
169            pnlTagResolver.setLayout(new BorderLayout());
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, Map<OsmPrimitiveType,Integer> sourceStatistics, Map<OsmPrimitiveType, Integer> targetStatistics) {
200            tagsForNodes = (tagsForNodes == null) ? new TagCollection() : tagsForNodes;
201            tagsForWays = (tagsForWays == null) ? new TagCollection() : tagsForWays;
202            tagsForRelations = (tagsForRelations == null) ? new TagCollection() : tagsForRelations;
203            if (tagsForNodes.isEmpty() && tagsForWays.isEmpty() && tagsForRelations.isEmpty()) {
204                populate(null,null,null);
205                return;
206            }
207            tpResolvers.removeAll();
208            initResolver(OsmPrimitiveType.NODE,tagsForNodes, targetStatistics);
209            initResolver(OsmPrimitiveType.WAY,tagsForWays, targetStatistics);
210            initResolver(OsmPrimitiveType.RELATION,tagsForRelations, targetStatistics);
211    
212            pnlTagResolver.setLayout(new BorderLayout());
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        class CancelAction extends AbstractAction {
266    
267            public CancelAction() {
268                putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
269                putValue(Action.NAME, tr("Cancel"));
270                putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
271                setEnabled(true);
272            }
273    
274            public void actionPerformed(ActionEvent arg0) {
275                setVisible(false);
276                setCanceled(true);
277            }
278        }
279    
280        class ApplyAction extends AbstractAction implements PropertyChangeListener {
281    
282            public ApplyAction() {
283                putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
284                putValue(Action.NAME, tr("Apply"));
285                putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
286                updateEnabledState();
287            }
288    
289            public void actionPerformed(ActionEvent arg0) {
290                setVisible(false);
291            }
292    
293            protected void updateEnabledState() {
294                if (mode == null) {
295                    setEnabled(false);
296                } else if (mode.equals(Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY)) {
297                    setEnabled(allPrimitivesResolver.getModel().isResolvedCompletely());
298                } else {
299                    boolean enabled = true;
300                    for (OsmPrimitiveType type: resolvers.keySet()) {
301                        enabled &= resolvers.get(type).getModel().isResolvedCompletely();
302                    }
303                    setEnabled(enabled);
304                }
305            }
306    
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(400,300))
320                ).applySafe(this);
321            } else {
322                new WindowGeometry(this).remember(getClass().getName() + ".geometry");
323            }
324            super.setVisible(visible);
325        }
326    
327        public TagCollection getResolution() {
328            return allPrimitivesResolver.getModel().getResolution();
329        }
330    
331        public TagCollection getResolution(OsmPrimitiveType type) {
332            if (type == null) return null;
333            return resolvers.get(type).getModel().getResolution();
334        }
335    
336        public void propertyChange(PropertyChangeEvent evt) {
337            if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
338                TagConflictResolverModel model = (TagConflictResolverModel)evt.getSource();
339                for (int i=0; i < tpResolvers.getTabCount();i++) {
340                    TagConflictResolver resolver = (TagConflictResolver)tpResolvers.getComponentAt(i);
341                    if (model == resolver.getModel()) {
342                        tpResolvers.setIconAt(i,
343                                (Boolean)evt.getNewValue() ? iconResolved : iconUnresolved
344    
345                        );
346                    }
347                }
348            }
349        }
350    
351        static public class StatisticsInfo {
352            public int numTags;
353            public Map<OsmPrimitiveType, Integer> sourceInfo;
354            public Map<OsmPrimitiveType, Integer> targetInfo;
355    
356            public StatisticsInfo() {
357                sourceInfo = new HashMap<OsmPrimitiveType, Integer>();
358                targetInfo = new HashMap<OsmPrimitiveType, Integer>();
359            }
360        }
361    
362        static private class StatisticsTableColumnModel extends DefaultTableColumnModel {
363            public StatisticsTableColumnModel() {
364                TableCellRenderer renderer = new StatisticsInfoRenderer();
365                TableColumn col = null;
366    
367                // column 0 - Paste
368                col = new TableColumn(0);
369                col.setHeaderValue(tr("Paste ..."));
370                col.setResizable(true);
371                col.setCellRenderer(renderer);
372                addColumn(col);
373    
374                // column 1 - From
375                col = new TableColumn(1);
376                col.setHeaderValue(tr("From ..."));
377                col.setResizable(true);
378                col.setCellRenderer(renderer);
379                addColumn(col);
380    
381                // column 2 - To
382                col = new TableColumn(2);
383                col.setHeaderValue(tr("To ..."));
384                col.setResizable(true);
385                col.setCellRenderer(renderer);
386                addColumn(col);
387            }
388        }
389    
390        static private class StatisticsTableModel extends DefaultTableModel {
391            private static final String[] HEADERS = new String[] {tr("Paste ..."), tr("From ..."), tr("To ...") };
392            private List<StatisticsInfo> data;
393    
394            public StatisticsTableModel() {
395                data = new ArrayList<StatisticsInfo>();
396            }
397    
398            @Override
399            public Object getValueAt(int row, int column) {
400                if (row == 0)
401                    return HEADERS[column];
402                else if (row -1 < data.size())
403                    return data.get(row -1);
404                else
405                    return null;
406            }
407    
408            @Override
409            public boolean isCellEditable(int row, int column) {
410                return false;
411            }
412    
413            @Override
414            public int getRowCount() {
415                if (data == null) return 1;
416                return data.size() + 1;
417            }
418    
419            public void reset() {
420                data.clear();
421            }
422    
423            public void append(StatisticsInfo info) {
424                data.add(info);
425                fireTableDataChanged();
426            }
427        }
428    
429        static private class StatisticsInfoRenderer extends JLabel implements TableCellRenderer {
430            protected void reset() {
431                setIcon(null);
432                setText("");
433                setFont(UIManager.getFont("Table.font"));
434            }
435            protected void renderNumTags(StatisticsInfo info) {
436                if (info == null) return;
437                setText(trn("{0} tag", "{0} tags", info.numTags, info.numTags));
438            }
439    
440            protected void renderStatistics(Map<OsmPrimitiveType, Integer> stat) {
441                if (stat == null) return;
442                if (stat.isEmpty()) return;
443                if (stat.size() == 1) {
444                    setIcon(ImageProvider.get(stat.keySet().iterator().next()));
445                } else {
446                    setIcon(ImageProvider.get("data", "object"));
447                }
448                String text = "";
449                for (OsmPrimitiveType type: stat.keySet()) {
450                    int numPrimitives = stat.get(type) == null ? 0 : stat.get(type);
451                    if (numPrimitives == 0) {
452                        continue;
453                    }
454                    String msg = "";
455                    switch(type) {
456                    case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives,numPrimitives); break;
457                    case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break;
458                    case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break;
459                    }
460                    text = text.equals("") ? msg : text + ", " + msg;
461                }
462                setText(text);
463            }
464    
465            protected void renderFrom(StatisticsInfo info) {
466                renderStatistics(info.sourceInfo);
467            }
468    
469            protected void renderTo(StatisticsInfo info) {
470                renderStatistics(info.targetInfo);
471            }
472    
473            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
474                    boolean hasFocus, int row, int column) {
475                reset();
476                if (value == null)
477                    return this;
478    
479                if (row == 0) {
480                    setFont(getFont().deriveFont(Font.BOLD));
481                    setText((String)value);
482                } else {
483                    StatisticsInfo info = (StatisticsInfo) value;
484    
485                    switch(column) {
486                    case 0: renderNumTags(info); break;
487                    case 1: renderFrom(info); break;
488                    case 2: renderTo(info); break;
489                    }
490                }
491                return this;
492            }
493        }
494    
495        static private class StatisticsInfoTable extends JPanel {
496    
497            private JTable infoTable;
498    
499            protected void build(StatisticsTableModel model) {
500                infoTable = new JTable(model, new StatisticsTableColumnModel());
501                infoTable.setShowHorizontalLines(true);
502                infoTable.setShowVerticalLines(false);
503                infoTable.setEnabled(false);
504                setLayout(new BorderLayout());
505                add(infoTable, BorderLayout.CENTER);
506            }
507    
508            public StatisticsInfoTable(StatisticsTableModel model) {
509                build(model);
510            }
511    
512            @Override
513            public Insets getInsets() {
514                Insets insets = super.getInsets();
515                insets.bottom = 20;
516                return insets;
517            }
518        }
519    }