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