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