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