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