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