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}