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 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();
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     */
122    public AddTagsDialog(String[][] tags, String senderName, Collection<? extends OsmPrimitive> primitives) {
123        super(Main.parent, tr("Add tags to selected objects"), new String[] {tr("Add selected tags"), tr("Add all tags"), tr("Cancel")},
124                false,
125                true);
126        setToolTipTexts(new String[]{tr("Add checked tags to selected objects"), tr("Shift+Enter: Add all tags to selected objects"), ""});
127
128        this.sender = senderName;
129
130        final DefaultTableModel tm = new DefaultTableModel(new String[] {tr("Assume"), tr("Key"), tr("Value"), tr("Existing values")},
131                tags.length) {
132            private final Class<?>[] types = {Boolean.class, String.class, Object.class, ExistingValues.class};
133            @Override
134            public Class<?> getColumnClass(int c) {
135                return types[c];
136            }
137        };
138
139        sel = primitives;
140        count = new int[tags.length];
141
142        for (int i = 0; i < tags.length; i++) {
143            count[i] = 0;
144            String key = tags[i][0];
145            String value = tags[i][1], oldValue;
146            Boolean b = Boolean.TRUE;
147            ExistingValues old = new ExistingValues(key);
148            for (OsmPrimitive osm : sel) {
149                oldValue  = osm.get(key);
150                if (oldValue != null) {
151                    old.addValue(oldValue);
152                    if (!oldValue.equals(value)) {
153                        b = Boolean.FALSE;
154                        count[i]++;
155                    }
156                }
157            }
158            tm.setValueAt(b, i, 0);
159            tm.setValueAt(tags[i][0], i, 1);
160            tm.setValueAt(tags[i][1].isEmpty() ? new DeleteTagMarker(count[i]) : tags[i][1], i, 2);
161            tm.setValueAt(old, i, 3);
162        }
163
164        propertyTable = new JTable(tm) {
165
166            @Override
167            public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
168                Component c = super.prepareRenderer(renderer, row, column);
169                if (count[row] > 0) {
170                    c.setFont(c.getFont().deriveFont(Font.ITALIC));
171                    c.setForeground(new Color(100, 100, 100));
172                } else {
173                    c.setFont(c.getFont().deriveFont(Font.PLAIN));
174                    c.setForeground(new Color(0, 0, 0));
175                }
176                return c;
177            }
178
179            @Override
180            public TableCellEditor getCellEditor(int row, int column) {
181                Object value = getValueAt(row, column);
182                if (value instanceof DeleteTagMarker) return null;
183                if (value instanceof ExistingValues) return null;
184                return getDefaultEditor(value.getClass());
185            }
186
187            @Override
188            public String getToolTipText(MouseEvent event) {
189                int r = rowAtPoint(event.getPoint());
190                int c = columnAtPoint(event.getPoint());
191                Object o = getValueAt(r, c);
192                if (c == 1 || c == 2) return o.toString();
193                if (c == 3) return ((ExistingValues) o).getToolTip();
194                return tr("Enable the checkbox to accept the value");
195            }
196        };
197
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_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();
215        tablePanel.setLayout(new GridBagLayout());
216        tablePanel.add(propertyTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
217        tablePanel.add(propertyTable, GBC.eol().fill(GBC.BOTH));
218        if (!sender.isEmpty() && !trustedSenders.contains(sender)) {
219            final JCheckBox c = new JCheckBox();
220            c.setAction(new AbstractAction(tr("Accept all tags from {0} for this session", sender)) {
221                @Override public void actionPerformed(ActionEvent e) {
222                    if (c.isSelected())
223                        trustedSenders.add(sender);
224                    else
225                        trustedSenders.remove(sender);
226                }
227            });
228            tablePanel.add(c, GBC.eol().insets(20, 10, 0, 0));
229        }
230        setContent(tablePanel);
231        setDefaultButton(2);
232    }
233
234    /**
235     * If you click the "Add tags" button build a ChangePropertyCommand for every key that has a checked checkbox
236     * to apply the key value pair to all selected osm objects.
237     * You get a entry for every key in the command queue.
238     */
239    @Override
240    protected void buttonAction(int buttonIndex, ActionEvent evt) {
241        // if layer all layers were closed, ignore all actions
242        if (Main.main.getCurrentDataSet() != null  && buttonIndex != 2) {
243            TableModel tm = propertyTable.getModel();
244            for (int i = 0; i < tm.getRowCount(); i++) {
245                if (buttonIndex == 1 || (Boolean) tm.getValueAt(i, 0)) {
246                    String key = (String) tm.getValueAt(i, 1);
247                    Object value = tm.getValueAt(i, 2);
248                    Main.main.undoRedo.add(new ChangePropertyCommand(sel,
249                            key, value instanceof String ? (String) value : ""));
250                }
251            }
252        }
253        if (buttonIndex == 2) {
254            trustedSenders.remove(sender);
255        }
256        setVisible(false);
257    }
258
259    /**
260     * parse addtags parameters Example URL (part):
261     * addtags=wikipedia:de%3DResidenzschloss Dresden|name:en%3DDresden Castle
262     */
263    public static void addTags(final Map<String, String> args, final String sender, final Collection<? extends OsmPrimitive> primitives) {
264        if (args.containsKey("addtags")) {
265            GuiHelper.executeByMainWorkerInEDT(new Runnable() {
266
267                @Override
268                public void run() {
269                    String[] tags = null;
270                    tags = Utils.decodeUrl(args.get("addtags")).split("\\|");
271                    Set<String> tagSet = new HashSet<>();
272                    for (String tag : tags) {
273                        if (!tag.trim().isEmpty() && tag.contains("=")) {
274                            tagSet.add(tag.trim());
275                        }
276                    }
277                    if (!tagSet.isEmpty()) {
278                        String[][] keyValue = new String[tagSet.size()][2];
279                        int i = 0;
280                        for (String tag : tagSet) {
281                            // support a  =   b===c as "a"="b===c"
282                            String[] pair = tag.split("\\s*=\\s*", 2);
283                            keyValue[i][0] = pair[0];
284                            keyValue[i][1] = pair.length < 2 ? "" : pair[1];
285                            i++;
286                        }
287                        addTags(keyValue, sender, primitives);
288                    }
289                }
290            });
291        }
292    }
293
294    /**
295     * Ask user and add the tags he confirm.
296     * @param keyValue is a table or {{tag1,val1},{tag2,val2},...}
297     * @param sender is a string for skipping confirmations. Use empty string for always confirmed adding.
298     * @param primitives OSM objects that will be modified
299     * @since 7521
300     */
301    public static void addTags(String[][] keyValue, String sender, Collection<? extends OsmPrimitive> primitives) {
302        if (trustedSenders.contains(sender)) {
303            if (Main.main.getCurrentDataSet() != null) {
304                for (String[] row : keyValue) {
305                    Main.main.undoRedo.add(new ChangePropertyCommand(primitives, row[0], row[1]));
306                }
307            }
308        } else {
309            new AddTagsDialog(keyValue, sender, primitives).showDialog();
310        }
311    }
312}