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