001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.remotecontrol;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Component;
008import java.awt.Font;
009import java.awt.GridBagLayout;
010import java.awt.event.ActionEvent;
011import java.awt.event.KeyEvent;
012import java.awt.event.MouseEvent;
013import java.io.UnsupportedEncodingException;
014import java.net.URLDecoder;
015import java.util.Collection;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.Map;
019import java.util.Set;
020
021import javax.swing.AbstractAction;
022import javax.swing.JCheckBox;
023import javax.swing.JPanel;
024import javax.swing.JTable;
025import javax.swing.KeyStroke;
026import javax.swing.table.DefaultTableModel;
027import javax.swing.table.TableCellEditor;
028import javax.swing.table.TableCellRenderer;
029import javax.swing.table.TableModel;
030
031import org.openstreetmap.josm.Main;
032import org.openstreetmap.josm.command.ChangePropertyCommand;
033import org.openstreetmap.josm.data.osm.OsmPrimitive;
034import org.openstreetmap.josm.gui.ExtendedDialog;
035import org.openstreetmap.josm.gui.util.GuiHelper;
036import org.openstreetmap.josm.gui.util.TableHelper;
037import org.openstreetmap.josm.tools.GBC;
038
039/**
040 * Dialog to add tags as part of the remotecontrol.
041 * Existing Keys get grey color and unchecked selectboxes so they will not overwrite the old Key-Value-Pairs by default.
042 * You can choose the tags you want to add by selectboxes. You can edit the tags before you apply them.
043 * @author master
044 * @since 3850
045 */
046public class AddTagsDialog extends ExtendedDialog {
047
048    private final JTable propertyTable;
049    private final Collection<? extends OsmPrimitive> sel;
050    private final int[] count;
051
052    private final String sender;
053    private static final Set<String> trustedSenders = new HashSet<>();
054
055    /**
056     * Class for displaying "delete from ... objects" in the table
057     */
058    static class DeleteTagMarker {
059        int num;
060        public DeleteTagMarker(int num) {
061            this.num = num;
062        }
063        @Override
064        public String toString() {
065            return tr("<delete from {0} objects>", num);
066        }
067    }
068
069    /**
070     * Class for displaying list of existing tag values in the table
071     */
072    static class ExistingValues {
073        String tag;
074        Map<String, Integer> valueCount;
075        public ExistingValues(String tag) {
076            this.tag=tag; valueCount=new HashMap<>();
077        }
078
079        int addValue(String val) {
080            Integer c = valueCount.get(val);
081            int r = c==null? 1 : (c.intValue()+1);
082            valueCount.put(val, r);
083            return r;
084        }
085
086        @Override
087        public String toString() {
088            StringBuilder sb=new StringBuilder();
089            for (String k: valueCount.keySet()) {
090                if (sb.length()>0) sb.append(", ");
091                sb.append(k);
092            }
093            return sb.toString();
094        }
095
096        private String getToolTip() {
097            StringBuilder sb=new StringBuilder();
098            sb.append("<html>");
099            sb.append(tr("Old values of"));
100            sb.append(" <b>");
101            sb.append(tag);
102            sb.append("</b><br/>");
103            for (String k: valueCount.keySet()) {
104                sb.append("<b>");
105                sb.append(valueCount.get(k));
106                sb.append(" x </b>");
107                sb.append(k);
108                sb.append("<br/>");
109            }
110            sb.append("</html>");
111            return sb.toString();
112
113        }
114    }
115
116    /**
117     * Constructs a new {@code AddTagsDialog}.
118     */
119    public AddTagsDialog(String[][] tags, String senderName, Collection<? extends OsmPrimitive> primitives) {
120        super(Main.parent, tr("Add tags to selected objects"), new String[] { tr("Add selected tags"), tr("Add all tags"),  tr("Cancel")},
121                false,
122                true);
123        setToolTipTexts(new String[]{tr("Add checked tags to selected objects"), tr("Shift+Enter: Add all tags to selected objects"), ""});
124
125        this.sender = senderName;
126
127        final DefaultTableModel tm = new DefaultTableModel(new String[] {tr("Assume"), tr("Key"), tr("Value"), tr("Existing values")}, tags.length) {
128            final Class<?>[] types = {Boolean.class, String.class, Object.class, ExistingValues.class};
129            @Override
130            public Class<?> getColumnClass(int c) {
131                return types[c];
132            }
133        };
134
135        sel = primitives;
136        count = new int[tags.length];
137
138        for (int i = 0; i<tags.length; i++) {
139            count[i] = 0;
140            String key = tags[i][0];
141            String value = tags[i][1], oldValue;
142            Boolean b = Boolean.TRUE;
143            ExistingValues old = new ExistingValues(key);
144            for (OsmPrimitive osm : sel) {
145                oldValue  = osm.get(key);
146                if (oldValue!=null) {
147                    old.addValue(oldValue);
148                    if (!oldValue.equals(value)) {
149                        b = Boolean.FALSE;
150                        count[i]++;
151                    }
152                }
153            }
154            tm.setValueAt(b, i, 0);
155            tm.setValueAt(tags[i][0], i, 1);
156            tm.setValueAt(tags[i][1].isEmpty() ? new DeleteTagMarker(count[i]) : tags[i][1], i, 2);
157            tm.setValueAt(old , i, 3);
158        }
159
160        propertyTable = new JTable(tm) {
161
162            @Override
163            public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
164                Component c = super.prepareRenderer(renderer, row, column);
165                if (count[row]>0) {
166                    c.setFont(c.getFont().deriveFont(Font.ITALIC));
167                    c.setForeground(new Color(100, 100, 100));
168                } else {
169                    c.setFont(c.getFont().deriveFont(Font.PLAIN));
170                    c.setForeground(new Color(0, 0, 0));
171                }
172                return c;
173            }
174
175            @Override
176            public TableCellEditor getCellEditor(int row, int column) {
177                Object value = getValueAt(row,column);
178                if (value instanceof DeleteTagMarker) return null;
179                if (value instanceof ExistingValues) return null;
180                return getDefaultEditor(value.getClass());
181            }
182
183            @Override
184            public String getToolTipText(MouseEvent event) {
185                int r = rowAtPoint(event.getPoint());
186                int c = columnAtPoint(event.getPoint());
187                Object o = getValueAt(r, c);
188                if (c==1 || c==2) return o.toString();
189                if (c==3) return ((ExistingValues)o).getToolTip();
190                return tr("Enable the checkbox to accept the value");
191            }
192        };
193
194        propertyTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
195        // a checkbox has a size of 15 px
196        propertyTable.getColumnModel().getColumn(0).setMaxWidth(15);
197        TableHelper.adjustColumnWidth(propertyTable, 1, 150);
198        TableHelper.adjustColumnWidth(propertyTable, 2, 400);
199        TableHelper.adjustColumnWidth(propertyTable, 3, 300);
200        // get edit results if the table looses the focus, for example if a user clicks "add tags"
201        propertyTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
202        propertyTable.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_MASK), "shiftenter");
203        propertyTable.getActionMap().put("shiftenter", new AbstractAction() {
204            @Override  public void actionPerformed(ActionEvent e) {
205                buttonAction(1, e); // add all tags on Shift-Enter
206            }
207        });
208
209        // set the content of this AddTagsDialog consisting of the tableHeader and the table itself.
210        JPanel tablePanel = new JPanel();
211        tablePanel.setLayout(new GridBagLayout());
212        tablePanel.add(propertyTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
213        tablePanel.add(propertyTable, GBC.eol().fill(GBC.BOTH));
214        if (!sender.isEmpty() && !trustedSenders.contains(sender)) {
215            final JCheckBox c = new JCheckBox();
216            c.setAction(new AbstractAction(tr("Accept all tags from {0} for this session", sender) ) {
217                @Override public void actionPerformed(ActionEvent e) {
218                    if (c.isSelected())
219                        trustedSenders.add(sender);
220                    else
221                        trustedSenders.remove(sender);
222                }
223            } );
224            tablePanel.add(c , GBC.eol().insets(20,10,0,0));
225        }
226        setContent(tablePanel);
227        setDefaultButton(2);
228    }
229
230    /**
231     * If you click the "Add tags" button build a ChangePropertyCommand for every key that has a checked checkbox to apply the key value pair to all selected osm objects.
232     * You get a entry for every key in the command queue.
233     */
234    @Override
235    protected void buttonAction(int buttonIndex, ActionEvent evt) {
236        // if layer all layers were closed, ignore all actions
237        if (Main.main.getCurrentDataSet() != null  && buttonIndex != 2) {
238            TableModel tm = propertyTable.getModel();
239            for (int i=0; i<tm.getRowCount(); i++) {
240                if (buttonIndex==1 || (Boolean)tm.getValueAt(i, 0)) {
241                    String key =(String)tm.getValueAt(i, 1);
242                    Object value = tm.getValueAt(i, 2);
243                    Main.main.undoRedo.add(new ChangePropertyCommand(sel,
244                            key, value instanceof String ? (String) value : ""));
245                }
246            }
247        }
248        if (buttonIndex == 2) {
249            trustedSenders.remove(sender);
250        }
251        setVisible(false);
252    }
253
254    /**
255     * parse addtags parameters Example URL (part):
256     * addtags=wikipedia:de%3DResidenzschloss Dresden|name:en%3DDresden Castle
257     */
258    public static void addTags(final Map<String, String> args, final String sender, final Collection<? extends OsmPrimitive> primitives) {
259        if (args.containsKey("addtags")) {
260            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
261
262                @Override
263                public void run() {
264                    String[] tags = null;
265                    try {
266                        tags = URLDecoder.decode(args.get("addtags"), "UTF-8").split("\\|");
267                    } catch (UnsupportedEncodingException e) {
268                        throw new RuntimeException(e);
269                    }
270                    Set<String> tagSet = new HashSet<>();
271                    for (String tag : tags) {
272                        if (!tag.trim().isEmpty() && tag.contains("=")) {
273                            tagSet.add(tag.trim());
274                        }
275                    }
276                    if (!tagSet.isEmpty()) {
277                        String[][] keyValue = new String[tagSet.size()][2];
278                        int i = 0;
279                        for (String tag : tagSet) {
280                            // support a  =   b===c as "a"="b===c"
281                            String [] pair = tag.split("\\s*=\\s*",2);
282                            keyValue[i][0] = pair[0];
283                            keyValue[i][1] = pair.length<2 ? "": pair[1];
284                            i++;
285                        }
286                        addTags(keyValue, sender, primitives);
287                    }
288                }
289            });
290        }
291    }
292
293    /**
294     * Ask user and add the tags he confirm.
295     * @param keyValue is a table or {{tag1,val1},{tag2,val2},...}
296     * @param sender is a string for skipping confirmations. Use empty string for always confirmed adding.
297     * @param primitives OSM objects that will be modified
298     * @since 7521
299     */
300    public static void addTags(String[][] keyValue, String sender, Collection<? extends OsmPrimitive> primitives) {
301        if (trustedSenders.contains(sender)) {
302            if (Main.main.getCurrentDataSet() != null) {
303                for (String[] row : keyValue) {
304                    Main.main.undoRedo.add(new ChangePropertyCommand(primitives, row[0], row[1]));
305                }
306            }
307        } else {
308            new AddTagsDialog(keyValue, sender, primitives).showDialog();
309        }
310    }
311}