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