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