// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.gui.tagging; import static org.openstreetmap.josm.tools.I18n.marktr; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trc; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.Component; import java.awt.Container; import java.awt.GridBagLayout; import java.awt.Image; import java.awt.event.ActionEvent; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.TreeSet; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.ChangePropertyCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmUtils; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.ExtendedDialog; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.QuadStateCheckBox; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionCache; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionItemPritority; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; import org.openstreetmap.josm.io.MirroredInputStream; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.UrlLabel; import org.openstreetmap.josm.tools.XmlObjectParser; import org.xml.sax.SAXException; /** * This class read encapsulate one tagging preset. A class method can * read in all predefined presets, either shipped with JOSM or that are * in the config directory. * * It is also able to construct dialogs out of preset definitions. */ public class TaggingPreset extends AbstractAction implements MapView.LayerChangeListener { public TaggingPresetMenu group = null; public String name; public String name_context; public String locale_name; public final static String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text"; private static File zipIcons = null; public static abstract class Item { protected void initAutoCompletionField(AutoCompletingTextField field, String key) { OsmDataLayer layer = Main.main.getEditLayer(); if (layer == null) return; AutoCompletionList list = new AutoCompletionList(); AutoCompletionCache.getCacheForLayer(Main.main.getEditLayer()).populateWithTagValues(list, key, false); field.setAutoCompletionList(list); } public boolean focus = false; abstract boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel); abstract void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds); boolean requestFocusInWindow() {return false;} } public static class Usage { TreeSet<String> values; boolean hadKeys = false; boolean hadEmpty = false; public boolean hasUniqueValue() { return values.size() == 1 && !hadEmpty; } public boolean unused() { return values.size() == 0; } public String getFirst() { return values.first(); } public boolean hadKeys() { return hadKeys; } } public static final String DIFFERENT = tr("<different>"); static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) { Usage returnValue = new Usage(); returnValue.values = new TreeSet<String>(); for (OsmPrimitive s : sel) { String v = s.get(key); if (v != null) { returnValue.values.add(v); } else { returnValue.hadEmpty = true; } returnValue.hadKeys = ! returnValue.values.isEmpty() | returnValue.hadEmpty; } return returnValue; } static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) { Usage returnValue = new Usage(); returnValue.values = new TreeSet<String>(); for (OsmPrimitive s : sel) { String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key)); if (booleanValue != null) { returnValue.values.add(booleanValue); } } return returnValue; } public static class Text extends Item { public String key; public String text; public String locale_text; public String text_context; public String default_; public String originalValue; public boolean use_last_as_default = false; public boolean delete_if_empty = false; private JComponent value; @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { // find out if our key is already used in the selection. Usage usage = determineTextUsage(sel, key); AutoCompletingTextField textField = new AutoCompletingTextField(); initAutoCompletionField(textField, key); if (usage.unused()){ if (use_last_as_default && lastValue.containsKey(key)) { textField.setText(lastValue.get(key)); } else { textField.setText(default_); } value = textField; originalValue = null; } else if (usage.hasUniqueValue()) { // all objects use the same value textField.setText(usage.getFirst()); value = textField; originalValue = usage.getFirst(); } else { // the objects have different values JComboBox comboBox = new JComboBox(usage.values.toArray()); comboBox.setEditable(true); comboBox.setEditor(textField); comboBox.getEditor().setItem(DIFFERENT); value=comboBox; originalValue = DIFFERENT; } if(locale_text == null) { if (text != null) { if(text_context != null) { locale_text = trc(text_context, text); } else { locale_text = tr(text); } } } p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0)); p.add(value, GBC.eol().fill(GBC.HORIZONTAL)); return true; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) { // return if unchanged String v = (value instanceof JComboBox) ? ((JComboBox)value).getEditor().getItem().toString() : ((JTextField)value).getText(); if (use_last_as_default) { lastValue.put(key, v); } if (v.equals(originalValue) || (originalValue == null && v.length() == 0)) return; if (delete_if_empty && v.length() == 0) { v = null; } cmds.add(new ChangePropertyCommand(sel, key, v)); } @Override boolean requestFocusInWindow() {return value.requestFocusInWindow();} } public static class Check extends Item { public String key; public String text; public String text_context; public String locale_text; public boolean default_ = false; // only used for tagless objects public boolean use_last_as_default = false; private QuadStateCheckBox check; private QuadStateCheckBox.State initialState; private boolean def; @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { // find out if our key is already used in the selection. Usage usage = determineBooleanUsage(sel, key); def = default_; if(locale_text == null) { if(text_context != null) { locale_text = trc(text_context, text); } else { locale_text = tr(text); } } String oneValue = null; for (String s : usage.values) { oneValue = s; } if (usage.values.size() < 2 && (oneValue == null || OsmUtils.trueval.equals(oneValue) || OsmUtils.falseval.equals(oneValue))) { if(def) { for (OsmPrimitive s : sel) if(s.hasKeys()) { def = false; } } // all selected objects share the same value which is either true or false or unset, // we can display a standard check box. initialState = OsmUtils.trueval.equals(oneValue) ? QuadStateCheckBox.State.SELECTED : OsmUtils.falseval.equals(oneValue) ? QuadStateCheckBox.State.NOT_SELECTED : def ? QuadStateCheckBox.State.SELECTED : QuadStateCheckBox.State.UNSET; check = new QuadStateCheckBox(locale_text, initialState, new QuadStateCheckBox.State[] { QuadStateCheckBox.State.SELECTED, QuadStateCheckBox.State.NOT_SELECTED, QuadStateCheckBox.State.UNSET }); } else { def = false; // the objects have different values, or one or more objects have something // else than true/false. we display a quad-state check box // in "partial" state. initialState = QuadStateCheckBox.State.PARTIAL; check = new QuadStateCheckBox(locale_text, QuadStateCheckBox.State.PARTIAL, new QuadStateCheckBox.State[] { QuadStateCheckBox.State.PARTIAL, QuadStateCheckBox.State.SELECTED, QuadStateCheckBox.State.NOT_SELECTED, QuadStateCheckBox.State.UNSET }); } p.add(check, GBC.eol().fill(GBC.HORIZONTAL)); return true; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) { // if the user hasn't changed anything, don't create a command. if (check.getState() == initialState && !def) return; // otherwise change things according to the selected value. cmds.add(new ChangePropertyCommand(sel, key, check.getState() == QuadStateCheckBox.State.SELECTED ? OsmUtils.trueval : check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? OsmUtils.falseval : null)); } @Override boolean requestFocusInWindow() {return check.requestFocusInWindow();} } public static class Combo extends Item { public String key; public String text; public String text_context; public String locale_text; public String values; public String values_context; public String display_values; public String locale_display_values; public String default_; public boolean delete_if_empty = false; public boolean editable = true; public boolean use_last_as_default = false; private JComboBox combo; private LinkedHashMap<String,String> lhm; private Usage usage; private String originalValue; @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { // find out if our key is already used in the selection. usage = determineTextUsage(sel, key); String[] value_array = values.split(","); String[] display_array; if(locale_display_values != null) { display_array = locale_display_values.split(","); } else if(display_values != null) { display_array = display_values.split(","); } else { display_array = value_array; } if (display_array.length != value_array.length) { System.err.println(tr("Broken tagging preset \"{0}-{1}\" - number of items in display_values must be the same as in values", key, text)); display_array = value_array; } lhm = new LinkedHashMap<String,String>(); if (!usage.hasUniqueValue() && !usage.unused()){ lhm.put(DIFFERENT, DIFFERENT); } for (int i=0; i<value_array.length; i++) { lhm.put(value_array[i], (locale_display_values == null) ? (values_context == null ? tr(display_array[i]) : tr(values_context, display_array[i])) : display_array[i]); } if(!usage.unused()){ for (String s : usage.values) { if (!lhm.containsKey(s)) { lhm.put(s, s); } } } if (default_ != null && !lhm.containsKey(default_)) { lhm.put(default_, default_); } if(!lhm.containsKey("")) { lhm.put("", ""); } combo = new JComboBox(lhm.values().toArray()); combo.setEditable(editable); combo.setMaximumRowCount(13); AutoCompletingTextField tf = new AutoCompletingTextField(); initAutoCompletionField(tf, key); tf.getAutoCompletionList().add(Arrays.asList(display_array), AutoCompletionItemPritority.IS_IN_STANDARD); combo.setEditor(tf); if (usage.hasUniqueValue() && !usage.unused()){ originalValue=usage.getFirst(); combo.setSelectedItem(lhm.get(originalValue)); } // use default only in case it is a totally new entry else if(default_ != null && !usage.hadKeys()) { combo.setSelectedItem(default_); originalValue=DIFFERENT; } else if(usage.unused()){ combo.setSelectedItem(""); originalValue=""; } else{ combo.setSelectedItem(DIFFERENT); originalValue=DIFFERENT; } if(locale_text == null) { if(text_context != null) { locale_text = trc(text_context, text); } else { locale_text = tr(text); } } p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0)); p.add(combo, GBC.eol().fill(GBC.HORIZONTAL)); return true; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) { Object obj = combo.getSelectedItem(); String display = (obj == null) ? null : obj.toString(); String value = null; if(display == null && combo.isEditable()) { display = combo.getEditor().getItem().toString(); } if (display != null) { for (String key : lhm.keySet()) { String k = lhm.get(key); if (k != null && k.equals(display)) { value=key; } } if(value == null) { value = display; } } else { value = ""; } // no change if same as before if (value.equals(originalValue) || (originalValue == null && value.length() == 0)) return; if (delete_if_empty && value.length() == 0) { value = null; } cmds.add(new ChangePropertyCommand(sel, key, value)); } @Override boolean requestFocusInWindow() {return combo.requestFocusInWindow();} } public static class Label extends Item { public String text; public String text_context; public String locale_text; @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { if(locale_text == null) { if(text_context != null) { locale_text = trc(text_context, text); } else { locale_text = tr(text); } } p.add(new JLabel(locale_text), GBC.eol()); return false; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {} } public static class Link extends Item { public String href; public String text; public String text_context; public String locale_text; public String locale_href; @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { if(locale_text == null) { if(text == null) { locale_text = tr("More information about this feature"); } else if(text_context != null) { locale_text = trc(text_context, text); } else { locale_text = tr(text); } } String url = locale_href; if (url == null) { url = href; } if (url != null) { p.add(new UrlLabel(url, locale_text), GBC.eol().anchor(GBC.WEST)); } return false; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {} } public static class Optional extends Item { // TODO: Draw a box around optional stuff @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { p.add(new JLabel(" "), GBC.eol()); // space p.add(new JLabel(tr("Optional Attributes:")), GBC.eol()); p.add(new JLabel(" "), GBC.eol()); // space return false; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {} } public static class Space extends Item { @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { p.add(new JLabel(" "), GBC.eol()); // space return false; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {} } public static class Key extends Item { public String key; public String value; @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { return false; } @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) { cmds.add(new ChangePropertyCommand(sel, key, value != null && !value.equals("") ? value : null)); } } /** * The types as preparsed collection. */ public List<String> types; public List<Item> data = new LinkedList<Item>(); private static HashMap<String,String> lastValue = new HashMap<String,String>(); /** * Create an empty tagging preset. This will not have any items and * will be an empty string as text. createPanel will return null. * Use this as default item for "do not select anything". */ public TaggingPreset() { MapView.addLayerChangeListener(this); updateEnabledState(); } /** * Change the display name without changing the toolbar value. */ public void setDisplayName() { putValue(Action.NAME, getName()); putValue("toolbar", "tagging_" + getRawName()); } public String getLocaleName() { if(locale_name == null) { if(name_context != null) { locale_name = trc(name_context, name); } else { locale_name = tr(name); } } return locale_name; } public String getName() { return group != null ? group.getName() + "/" + getLocaleName() : getLocaleName(); } public String getRawName() { return group != null ? group.getRawName() + "/" + name : name; } /** * Called from the XML parser to set the icon * * FIXME for Java 1.6 - use 24x24 icons for LARGE_ICON_KEY (button bar) * and the 16x16 icons for SMALL_ICON. */ public void setIcon(String iconName) { Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null); ImageIcon icon = ImageProvider.getIfAvailable(s, "presets", null, iconName, zipIcons); if (icon == null) { System.out.println("Could not get presets icon " + iconName); icon = new ImageIcon(iconName); } if (Math.max(icon.getIconHeight(), icon.getIconWidth()) != 16) { icon = new ImageIcon(icon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH)); } putValue(Action.SMALL_ICON, icon); } /** * Called from the XML parser to set the types this preset affects */ private static Collection<String> allowedtypes = Arrays.asList(new String[] {marktr("way"), marktr("node"), marktr("relation"), marktr("closedway")}); public void setType(String types) throws SAXException { this.types = Arrays.asList(types.split(",")); for (String type : this.types) { if(!allowedtypes.contains(type)) throw new SAXException(tr("Unknown type: {0}", type)); } } public static List<TaggingPreset> readAll(Reader in) throws SAXException { XmlObjectParser parser = new XmlObjectParser(); parser.mapOnStart("item", TaggingPreset.class); parser.mapOnStart("separator", TaggingPresetSeparator.class); parser.mapBoth("group", TaggingPresetMenu.class); parser.map("text", Text.class); parser.map("link", Link.class); parser.mapOnStart("optional", Optional.class); parser.map("check", Check.class); parser.map("combo", Combo.class); parser.map("label", Label.class); parser.map("space", Space.class); parser.map("key", Key.class); LinkedList<TaggingPreset> all = new LinkedList<TaggingPreset>(); TaggingPresetMenu lastmenu = null; parser.start(in); while(parser.hasNext()) { Object o = parser.next(); if (o instanceof TaggingPresetMenu) { TaggingPresetMenu tp = (TaggingPresetMenu) o; if(tp == lastmenu) { lastmenu = tp.group; } else { tp.group = lastmenu; tp.setDisplayName(); lastmenu = tp; all.add(tp); Main.toolbar.register(tp); } } else if (o instanceof TaggingPresetSeparator) { TaggingPresetSeparator tp = (TaggingPresetSeparator) o; tp.group = lastmenu; all.add(tp); } else if (o instanceof TaggingPreset) { TaggingPreset tp = (TaggingPreset) o; tp.group = lastmenu; tp.setDisplayName(); all.add(tp); Main.toolbar.register(tp); } else { all.getLast().data.add((Item)o); } } return all; } public static Collection<TaggingPreset> readFromPreferences() { LinkedList<TaggingPreset> allPresets = new LinkedList<TaggingPreset>(); LinkedList<String> sources = new LinkedList<String>(); if(Main.pref.getBoolean("taggingpreset.enable-defaults", true)) { sources.add("resource://data/defaultpresets.xml"); } sources.addAll(Main.pref.getCollection("taggingpreset.sources", new LinkedList<String>())); for(String source : sources) { try { MirroredInputStream s = new MirroredInputStream(source); InputStream zip = s.getZipEntry("xml","preset"); if(zip != null) { zipIcons = s.getFile(); } InputStreamReader r; try { r = new InputStreamReader(zip == null ? s : zip, "UTF-8"); } catch (UnsupportedEncodingException e) { r = new InputStreamReader(zip == null ? s: zip); } allPresets.addAll(TaggingPreset.readAll(new BufferedReader(r))); } catch (IOException e) { e.printStackTrace(); JOptionPane.showMessageDialog( Main.parent, tr("Could not read tagging preset source: {0}",source), tr("Error"), JOptionPane.ERROR_MESSAGE ); } catch (SAXException e) { e.printStackTrace(); JOptionPane.showMessageDialog( Main.parent, tr("Error parsing {0}: ", source)+e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE ); } zipIcons = null; } return allPresets; } private static class PresetPanel extends JPanel { boolean hasElements = false; PresetPanel() { super(new GridBagLayout()); } } public PresetPanel createPanel(Collection<OsmPrimitive> selected) { if (data == null) return null; OsmDataLayer layer = Main.main.getEditLayer(); if (layer != null) { AutoCompletionCache.getCacheForLayer(layer).initFromDataSet(); } PresetPanel p = new PresetPanel(); LinkedList<Item> l = new LinkedList<Item>(); if(types != null){ JPanel pp = new JPanel(); for(String t : types){ JLabel la = new JLabel(ImageProvider.get("Mf_" + t)); la.setToolTipText(tr("Elements of type {0} are supported.", tr(t))); pp.add(la); } p.add(pp, GBC.eol()); } JPanel items = new JPanel(new GridBagLayout()); for (Item i : data){ if(i instanceof Link) { l.add(i); } else { if(i.addToPanel(items, selected)) { p.hasElements = true; } } } p.add(items, GBC.eol().fill()); if (selected.size() == 0) { setEnabledRec(items, false); } for(Item link : l) { link.addToPanel(p, selected); } return p; } /** * setEnabled() does not propagate to child elements, so we need this workaround. */ static void setEnabledRec(Container root, boolean enabled) { root.setEnabled(enabled); Component children[] = root.getComponents(); for(int i = 0; i < children.length; i++) { if(children[i] instanceof Container) { setEnabledRec((Container)children[i], enabled); } else { children[i].setEnabled(enabled); } } } public boolean isShowable() { for(Item i : data) { if(!(i instanceof Optional || i instanceof Space || i instanceof Key)) return true; } return false; } public void actionPerformed(ActionEvent e) { if (Main.main == null) return; if (Main.main.getCurrentDataSet() == null) return; Collection<OsmPrimitive> sel = createSelection(Main.main.getCurrentDataSet().getSelected()); PresetPanel p = createPanel(sel); if (p == null) return; int answer = 1; if (p.getComponentCount() != 0 && (sel.size() == 0 || p.hasElements)) { String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size()); if(sel.size() == 0) { if(originalSelectionEmpty) { title = tr("Nothing selected!"); } else { title = tr("Selection unsuitable!"); } } class PresetDialog extends ExtendedDialog { public PresetDialog(Component content, String title, boolean disableApply) { super(Main.parent, title, new String[] { tr("Apply Preset"), tr("Cancel") }, true); contentConstraints = GBC.eol().fill().insets(5,10,5,0); setButtonIcons(new String[] {"ok.png", "cancel.png" }); setContent(content); setupDialog(); buttons.get(0).setEnabled(!disableApply); buttons.get(0).setToolTipText(title); setVisible(true); } } answer = new PresetDialog(p, title, (sel.size() == 0)).getValue(); } if (sel.size() != 0 && answer == 1) { Command cmd = createCommand(sel); if (cmd != null) { Main.main.undoRedo.add(cmd); } } Main.main.getCurrentDataSet().setSelected(Main.main.getCurrentDataSet().getSelected()); // force update } /** * True whenever the original selection given into createSelection was empty */ private boolean originalSelectionEmpty = false; /** * Removes all unsuitable OsmPrimitives from the given list * @param participants List of possibile OsmPrimitives to tag * @return Cleaned list with suitable OsmPrimitives only */ private Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) { originalSelectionEmpty = participants.size() == 0; Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>(); for (OsmPrimitive osm : participants) { if (types != null) { if(osm instanceof Relation) { if(!types.contains("relation")) { continue; } } else if(osm instanceof Node) { if(!types.contains("node")) { continue; } } else if(osm instanceof Way) { if(!types.contains("way") && !(types.contains("closedway") && ((Way)osm).isClosed())) { continue; } } } sel.add(osm); } return sel; } private Command createCommand(Collection<OsmPrimitive> sel) { List<Command> cmds = new LinkedList<Command>(); for (Item i : data) { i.addCommands(sel, cmds); } if (cmds.size() == 0) return null; else if (cmds.size() == 1) return cmds.get(0); else return new SequenceCommand(tr("Change Properties"), cmds); } protected void updateEnabledState() { setEnabled(Main.main != null && Main.main.getCurrentDataSet() != null); } public void activeLayerChange(Layer oldLayer, Layer newLayer) { updateEnabledState(); } public void layerAdded(Layer newLayer) { updateEnabledState(); } public void layerRemoved(Layer oldLayer) { updateEnabledState(); } }