// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.ohe; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.Component; import java.awt.Font; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.ButtonGroup; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.JosmAction; import org.openstreetmap.josm.command.ChangePropertyCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.gui.MainMenu; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.plugins.Plugin; import org.openstreetmap.josm.plugins.PluginInformation; import org.openstreetmap.josm.plugins.ohe.gui.OheDialogPanel; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.Shortcut; /** * Opening hours editor plugin. */ public class OhePlugin extends Plugin { /** * Strings for choosing which key of an object with given tags should be * edited, the order is referencing the preference of the keys, String[] -> * {key, value, key-to-edit} key and value can contain regular expressions */ private final String[][] TAG_EDIT_STRINGS = new String[][] { {"opening_hours", ".*", "opening_hours"}, {"collection_times", ".*", "collection_times"}, {"collection_times:local", ".*", "collection_times:local"}, {"shop", ".*", "opening_hours"}, {"amenity", "post_box", "collection_times"}, {"amenity", "recycling", "collection_times"}, {"amenity", ".*", "opening_hours"}, {"lit", ".*", "lit"}, {"highway", ".*", "lit"} }; /** * Will be invoked by JOSM to bootstrap the plugin * * @param info information about the plugin and its local installation */ public OhePlugin(PluginInformation info) { super(info); MainMenu.add(Main.main.menu.dataMenu, new OheMenuAction(), false, 0); } /** * this Action is used for calling the OpeningsHourEditor, the selected * objects in the active datalayer are edited */ class OheMenuAction extends JosmAction { private static final long serialVersionUID = 1456257438391417756L; OheMenuAction() { super(tr("Edit opening hours"), "opening_hours.png", tr("Edit time-tag of selected element in a graphical interface"), Shortcut.registerShortcut( "tools:opening_hourseditor", tr("Tool: {0}", tr("Edit opening hours")), KeyEvent.VK_O, Shortcut.ALT_CTRL_SHIFT), true); } @Override protected void updateEnabledState() { if (getLayerManager().getEditDataSet() == null) { // if there is no current dataset, then the action is disabled setEnabled(false); } else { updateEnabledState(getLayerManager().getEditDataSet().getSelected()); } } @Override protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { // only enable the action if something is selected setEnabled(selection != null && !selection.isEmpty()); } @SuppressWarnings("unchecked") @Override public void actionPerformed(ActionEvent evt) { // fetch active Layer OsmDataLayer osmlayer = getLayerManager().getEditLayer(); if (osmlayer == null) return; Collection<OsmPrimitive> selection = osmlayer.data.getSelected(); // handling of multiple objects and their tags // copied from // org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog[rev4079][line802] Map<String, Integer> keyCount = new HashMap<>(); Map<String, Map<String, Integer>> valueCount = new TreeMap<>(); for (OsmPrimitive osm : selection) { for (String key : osm.keySet()) { String value = osm.get(key); keyCount.put(key, keyCount.containsKey(key) ? keyCount.get(key) + 1 : 1); if (valueCount.containsKey(key)) { Map<String, Integer> v = valueCount.get(key); v.put(value, v.containsKey(value) ? v.get(value) + 1 : 1); } else { TreeMap<String, Integer> v = new TreeMap<>(); v.put(value, 1); valueCount.put(key, v); } } } DefaultTableModel propertyData = new DefaultTableModel() { @Override public boolean isCellEditable(int row, int column) { return false; } @Override public Class<?> getColumnClass(int columnIndex) { return String.class; } }; propertyData.setColumnIdentifiers(new String[] {tr("Key"), tr("Value")}); for (Entry<String, Map<String, Integer>> e : valueCount.entrySet()) { int count = 0; for (Entry<String, Integer> e1 : e.getValue().entrySet()) { count += e1.getValue(); } if (count < selection.size()) { e.getValue().put("", selection.size() - count); } propertyData.addRow(new Object[] {e.getKey(), e.getValue()}); } final JTable propertyTable = new JTable(propertyData); propertyTable.getColumnModel().getColumn(1).setCellRenderer(new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column); if (value == null) return this; if (c instanceof JLabel) { String str = null; if (value instanceof String) { str = (String) value; } else if (value instanceof Map<?, ?>) { Map<?, ?> v = (Map<?, ?>) value; if (v.size() != 1) { str = tr("<different>"); c.setFont(c.getFont().deriveFont(Font.ITALIC)); } else { final Map.Entry<?, ?> entry = v.entrySet().iterator().next(); str = (String) entry.getKey(); } } ((JLabel) c).setText(str); } return c; } }); // end copy // showing the tags in a dialog propertyTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); JScrollPane sp = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); sp.setViewportView(propertyTable); final JComboBox<String> newTagField = new JComboBox<>(new String[]{ "opening_hours", "collection_times", "collection_times:local", "service_times", "lit"}); JRadioButton editButton = new JRadioButton(new AbstractAction(tr("edit existing tag")) { @Override public void actionPerformed(ActionEvent e) { propertyTable.setEnabled(true); newTagField.setEnabled(false); } }); JRadioButton newButton = new JRadioButton(new AbstractAction(tr("edit new tag")) { @Override public void actionPerformed(ActionEvent e) { propertyTable.setEnabled(false); newTagField.setEnabled(true); } }); ButtonGroup group = new ButtonGroup(); group.add(newButton); group.add(editButton); // search through the tags and choose which one should be selected String preSelectedKey = ""; searchLoop: for (String[] pattern : TAG_EDIT_STRINGS) { Pattern keyPattern = Pattern.compile(pattern[0]); Pattern valuePattern = Pattern.compile(pattern[1]); for (int i = 0; i < propertyData.getRowCount(); ++i) { Matcher keyMatcher = keyPattern.matcher((String) propertyData.getValueAt(i, 0)); if (keyMatcher.matches()) { Object value = propertyData.getValueAt(i, 1); if (value instanceof String && valuePattern.matcher((String) value).matches()) { preSelectedKey = pattern[2]; break searchLoop; } else if (value instanceof Map<?, ?>) { for (String v : ((Map<String, Integer>) value).keySet()) { if (valuePattern.matcher(v).matches()) { preSelectedKey = pattern[2]; break searchLoop; } } } } } } int preSelectedRow = -1; for (int i = 0; i < propertyData.getRowCount(); ++i) { if (preSelectedKey.equals(propertyData.getValueAt(i, 0))) { preSelectedRow = i; } } if (preSelectedRow != -1) { propertyTable.setEnabled(true); newTagField.setEnabled(false); propertyTable.setRowSelectionInterval(preSelectedRow, preSelectedRow); editButton.setSelected(true); } else { propertyTable.setEnabled(false); newTagField.setEnabled(true); newTagField.setSelectedItem(preSelectedKey); newButton.setSelected(true); } // load the preference for the clocksystem (12h/24h) ClockSystem clockSystem = ClockSystem.valueOf(Main.pref.get("ohe.clocksystem", ClockSystem.getClockSystem(Locale.getDefault()).toString())); JCheckBox useTwelveHourClock = new JCheckBox(tr("Display clock in 12h mode."), clockSystem == ClockSystem.TWELVE_HOURS); JPanel dlgPanel = new JPanel(new GridBagLayout()); dlgPanel.add(editButton, GBC.std().anchor(GBC.WEST)); dlgPanel.add(sp, GBC.eol().fill(GBC.BOTH)); dlgPanel.add(newButton, GBC.std().anchor(GBC.WEST)); dlgPanel.add(newTagField, GBC.eol().fill(GBC.HORIZONTAL)); dlgPanel.add(useTwelveHourClock, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); JOptionPane optionPane = new JOptionPane(dlgPanel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION); JDialog dlg = optionPane.createDialog(Main.parent, tr("Choose key")); dlg.pack(); dlg.setResizable(true); dlg.setVisible(true); Object answer = optionPane.getValue(); String keyToEdit = null; Object valuesToEdit = ""; if (answer != null && answer != JOptionPane.UNINITIALIZED_VALUE && (answer instanceof Integer && (Integer) answer == JOptionPane.OK_OPTION)) if (editButton.isSelected() && propertyTable.getSelectedRow() != -1) { keyToEdit = (String) propertyData.getValueAt(propertyTable.getSelectedRow(), 0); valuesToEdit = propertyData.getValueAt(propertyTable.getSelectedRow(), 1); } else if (newButton.isSelected()) { keyToEdit = newTagField.getSelectedItem().toString(); } if (keyToEdit == null) return; // save the value for the clocksystem (12h/24h) Main.pref.put("ohe.clocksystem", (useTwelveHourClock.isSelected() ? ClockSystem.TWELVE_HOURS : ClockSystem.TWENTYFOUR_HOURS).toString()); OheDialogPanel panel = new OheDialogPanel(OhePlugin.this, keyToEdit, valuesToEdit, useTwelveHourClock.isSelected() ? ClockSystem.TWELVE_HOURS : ClockSystem.TWENTYFOUR_HOURS); optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION); dlg = optionPane.createDialog(Main.parent, tr("Edit")); dlg.setResizable(true); dlg.setVisible(true); String[] changedKeyValuePair = null; answer = optionPane.getValue(); if (!(answer == null || answer == JOptionPane.UNINITIALIZED_VALUE || (answer instanceof Integer && (Integer) answer != JOptionPane.OK_OPTION))) { changedKeyValuePair = panel.getChangedKeyValuePair(); } if (changedKeyValuePair == null) return; String key = changedKeyValuePair[0].trim(); String newkey = changedKeyValuePair[1].trim(); String value = changedKeyValuePair[2].trim(); if (value.equals("")) { value = null; // delete the key } if (newkey.equals("")) { newkey = key; value = null; // delete the key instead } if (key.equals(newkey) && tr("<different>").equals(value)) return; if (key.equals(newkey) || value == null) { Main.main.undoRedo.add(new ChangePropertyCommand(selection, newkey, value)); } else { Collection<Command> commands = new Vector<>(); commands.add(new ChangePropertyCommand(selection, key, null)); if (value.equals(tr("<different>"))) { HashMap<String, Vector<OsmPrimitive>> map = new HashMap<>(); for (OsmPrimitive osm : selection) { String val = osm.get(key); if (val != null) { if (map.containsKey(val)) { map.get(val).add(osm); } else { Vector<OsmPrimitive> v = new Vector<>(); v.add(osm); map.put(val, v); } } } for (Entry<String, Vector<OsmPrimitive>> e : map.entrySet()) { commands.add(new ChangePropertyCommand(e.getValue(), newkey, e.getKey())); } } else { commands.add(new ChangePropertyCommand(selection, newkey, value)); } Main.main.undoRedo.add(new SequenceCommand(trn("Change properties of up to {0} object", "Change properties of up to {0} objects", selection.size(), selection.size()), commands)); } } } }