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