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