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