// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.io.remotecontrol; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.JCheckBox; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableModel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.ChangePropertyCommand; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.gui.ExtendedDialog; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.gui.util.TableHelper; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.Utils; /** * Dialog to add tags as part of the remotecontrol. * Existing Keys get grey color and unchecked selectboxes so they will not overwrite the old Key-Value-Pairs by default. * You can choose the tags you want to add by selectboxes. You can edit the tags before you apply them. * @author master * @since 3850 */ public class AddTagsDialog extends ExtendedDialog { private final JTable propertyTable; private final transient Collection<? extends OsmPrimitive> sel; private final int[] count; private final String sender; private static final Set<String> trustedSenders = new HashSet<>(); static final class PropertyTableModel extends DefaultTableModel { private final Class<?>[] types = {Boolean.class, String.class, Object.class, ExistingValues.class}; PropertyTableModel(int rowCount) { super(new String[] {tr("Assume"), tr("Key"), tr("Value"), tr("Existing values")}, rowCount); } @Override public Class<?> getColumnClass(int c) { return types[c]; } } /** * Class for displaying "delete from ... objects" in the table */ static class DeleteTagMarker { private final int num; DeleteTagMarker(int num) { this.num = num; } @Override public String toString() { return tr("<delete from {0} objects>", num); } } /** * Class for displaying list of existing tag values in the table */ static class ExistingValues { private final String tag; private final Map<String, Integer> valueCount; ExistingValues(String tag) { this.tag = tag; this.valueCount = new HashMap<>(); } int addValue(String val) { Integer c = valueCount.get(val); int r = c == null ? 1 : (c.intValue()+1); valueCount.put(val, r); return r; } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (String k: valueCount.keySet()) { if (sb.length() > 0) sb.append(", "); sb.append(k); } return sb.toString(); } private String getToolTip() { StringBuilder sb = new StringBuilder(64); sb.append("<html>") .append(tr("Old values of")) .append(" <b>") .append(tag) .append("</b><br/>"); for (Entry<String, Integer> e : valueCount.entrySet()) { sb.append("<b>") .append(e.getValue()) .append(" x </b>") .append(e.getKey()) .append("<br/>"); } sb.append("</html>"); return sb.toString(); } } /** * Constructs a new {@code AddTagsDialog}. * @param tags tags to add * @param senderName String for skipping confirmations. Use empty string for always confirmed adding. * @param primitives OSM objects that will be modified */ public AddTagsDialog(String[][] tags, String senderName, Collection<? extends OsmPrimitive> primitives) { super(Main.parent, tr("Add tags to selected objects"), new String[] {tr("Add selected tags"), tr("Add all tags"), tr("Cancel")}, false, true); setToolTipTexts(new String[]{tr("Add checked tags to selected objects"), tr("Shift+Enter: Add all tags to selected objects"), ""}); this.sender = senderName; final DefaultTableModel tm = new PropertyTableModel(tags.length); sel = primitives; count = new int[tags.length]; for (int i = 0; i < tags.length; i++) { count[i] = 0; String key = tags[i][0]; String value = tags[i][1], oldValue; Boolean b = Boolean.TRUE; ExistingValues old = new ExistingValues(key); for (OsmPrimitive osm : sel) { oldValue = osm.get(key); if (oldValue != null) { old.addValue(oldValue); if (!oldValue.equals(value)) { b = Boolean.FALSE; count[i]++; } } } tm.setValueAt(b, i, 0); tm.setValueAt(tags[i][0], i, 1); tm.setValueAt(tags[i][1].isEmpty() ? new DeleteTagMarker(count[i]) : tags[i][1], i, 2); tm.setValueAt(old, i, 3); } propertyTable = new JTable(tm) { @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component c = super.prepareRenderer(renderer, row, column); if (count[row] > 0) { c.setFont(c.getFont().deriveFont(Font.ITALIC)); c.setForeground(new Color(100, 100, 100)); } else { c.setFont(c.getFont().deriveFont(Font.PLAIN)); c.setForeground(new Color(0, 0, 0)); } return c; } @Override public TableCellEditor getCellEditor(int row, int column) { Object value = getValueAt(row, column); if (value instanceof DeleteTagMarker) return null; if (value instanceof ExistingValues) return null; return getDefaultEditor(value.getClass()); } @Override public String getToolTipText(MouseEvent event) { int r = rowAtPoint(event.getPoint()); int c = columnAtPoint(event.getPoint()); if (r < 0 || c < 0) { return getToolTipText(); } Object o = getValueAt(r, c); if (c == 1 || c == 2) return o.toString(); if (c == 3) return ((ExistingValues) o).getToolTip(); return tr("Enable the checkbox to accept the value"); } }; propertyTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); // a checkbox has a size of 15 px propertyTable.getColumnModel().getColumn(0).setMaxWidth(15); TableHelper.adjustColumnWidth(propertyTable, 1, 150); TableHelper.adjustColumnWidth(propertyTable, 2, 400); TableHelper.adjustColumnWidth(propertyTable, 3, 300); // get edit results if the table looses the focus, for example if a user clicks "add tags" propertyTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); propertyTable.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_MASK), "shiftenter"); propertyTable.getActionMap().put("shiftenter", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { buttonAction(1, e); // add all tags on Shift-Enter } }); // set the content of this AddTagsDialog consisting of the tableHeader and the table itself. JPanel tablePanel = new JPanel(new GridBagLayout()); tablePanel.add(propertyTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL)); tablePanel.add(propertyTable, GBC.eol().fill(GBC.BOTH)); if (!sender.isEmpty() && !trustedSenders.contains(sender)) { final JCheckBox c = new JCheckBox(); c.setAction(new AbstractAction(tr("Accept all tags from {0} for this session", sender)) { @Override public void actionPerformed(ActionEvent e) { if (c.isSelected()) trustedSenders.add(sender); else trustedSenders.remove(sender); } }); tablePanel.add(c, GBC.eol().insets(20, 10, 0, 0)); } setContent(tablePanel); setDefaultButton(2); } /** * 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. * You get a entry for every key in the command queue. */ @Override protected void buttonAction(int buttonIndex, ActionEvent evt) { // if layer all layers were closed, ignore all actions if (buttonIndex != 2 && Main.getLayerManager().getEditDataSet() != null) { TableModel tm = propertyTable.getModel(); for (int i = 0; i < tm.getRowCount(); i++) { if (buttonIndex == 1 || (Boolean) tm.getValueAt(i, 0)) { String key = (String) tm.getValueAt(i, 1); Object value = tm.getValueAt(i, 2); Main.main.undoRedo.add(new ChangePropertyCommand(sel, key, value instanceof String ? (String) value : "")); } } } if (buttonIndex == 2) { trustedSenders.remove(sender); } setVisible(false); } /** * parse addtags parameters Example URL (part): * addtags=wikipedia:de%3DResidenzschloss Dresden|name:en%3DDresden Castle * @param args request arguments * @param sender is a string for skipping confirmations. Use empty string for always confirmed adding. * @param primitives OSM objects that will be modified */ public static void addTags(final Map<String, String> args, final String sender, final Collection<? extends OsmPrimitive> primitives) { if (args.containsKey("addtags")) { GuiHelper.executeByMainWorkerInEDT(() -> { Set<String> tagSet = new HashSet<>(); for (String tag1 : Utils.decodeUrl(args.get("addtags")).split("\\|")) { if (!tag1.trim().isEmpty() && tag1.contains("=")) { tagSet.add(tag1.trim()); } } if (!tagSet.isEmpty()) { String[][] keyValue = new String[tagSet.size()][2]; int i = 0; for (String tag2 : tagSet) { // support a = b===c as "a"="b===c" String[] pair = tag2.split("\\s*=\\s*", 2); keyValue[i][0] = pair[0]; keyValue[i][1] = pair.length < 2 ? "" : pair[1]; i++; } addTags(keyValue, sender, primitives); } }); } } /** * Ask user and add the tags he confirm. * @param keyValue is a table or {{tag1,val1},{tag2,val2},...} * @param sender is a string for skipping confirmations. Use empty string for always confirmed adding. * @param primitives OSM objects that will be modified * @since 7521 */ public static void addTags(String[][] keyValue, String sender, Collection<? extends OsmPrimitive> primitives) { if (trustedSenders.contains(sender)) { if (Main.getLayerManager().getEditDataSet() != null) { for (String[] row : keyValue) { Main.main.undoRedo.add(new ChangePropertyCommand(primitives, row[0], row[1])); } } } else { new AddTagsDialog(keyValue, sender, primitives).showDialog(); } } }