001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.gui.tagging;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trc;
006    import static org.openstreetmap.josm.tools.I18n.trn;
007    
008    import java.awt.Component;
009    import java.awt.Dimension;
010    import java.awt.Font;
011    import java.awt.GridBagLayout;
012    import java.awt.Insets;
013    import java.awt.event.ActionEvent;
014    import java.io.BufferedReader;
015    import java.io.File;
016    import java.io.IOException;
017    import java.io.InputStream;
018    import java.io.InputStreamReader;
019    import java.io.Reader;
020    import java.io.UnsupportedEncodingException;
021    import java.util.ArrayList;
022    import java.util.Arrays;
023    import java.util.Collection;
024    import java.util.Collections;
025    import java.util.EnumSet;
026    import java.util.HashMap;
027    import java.util.HashSet;
028    import java.util.LinkedHashMap;
029    import java.util.LinkedList;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.TreeSet;
033    
034    import javax.swing.AbstractAction;
035    import javax.swing.Action;
036    import javax.swing.ImageIcon;
037    import javax.swing.JComponent;
038    import javax.swing.JLabel;
039    import javax.swing.JList;
040    import javax.swing.JOptionPane;
041    import javax.swing.JPanel;
042    import javax.swing.JScrollPane;
043    import javax.swing.JTextField;
044    import javax.swing.ListCellRenderer;
045    import javax.swing.ListModel;
046    import javax.swing.SwingUtilities;
047    
048    import org.openstreetmap.josm.Main;
049    import org.openstreetmap.josm.actions.search.SearchCompiler;
050    import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
051    import org.openstreetmap.josm.command.ChangePropertyCommand;
052    import org.openstreetmap.josm.command.Command;
053    import org.openstreetmap.josm.command.SequenceCommand;
054    import org.openstreetmap.josm.data.osm.Node;
055    import org.openstreetmap.josm.data.osm.OsmPrimitive;
056    import org.openstreetmap.josm.data.osm.OsmUtils;
057    import org.openstreetmap.josm.data.osm.Relation;
058    import org.openstreetmap.josm.data.osm.RelationMember;
059    import org.openstreetmap.josm.data.osm.Tag;
060    import org.openstreetmap.josm.data.osm.Way;
061    import org.openstreetmap.josm.data.preferences.BooleanProperty;
062    import org.openstreetmap.josm.gui.ExtendedDialog;
063    import org.openstreetmap.josm.gui.MapView;
064    import org.openstreetmap.josm.gui.QuadStateCheckBox;
065    import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
066    import org.openstreetmap.josm.gui.layer.Layer;
067    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
068    import org.openstreetmap.josm.gui.preferences.SourceEntry;
069    import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference.PresetPrefHelper;
070    import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
071    import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionItemPritority;
072    import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
073    import org.openstreetmap.josm.gui.util.GuiHelper;
074    import org.openstreetmap.josm.gui.widgets.JosmComboBox;
075    import org.openstreetmap.josm.io.MirroredInputStream;
076    import org.openstreetmap.josm.tools.GBC;
077    import org.openstreetmap.josm.tools.ImageProvider;
078    import org.openstreetmap.josm.tools.UrlLabel;
079    import org.openstreetmap.josm.tools.Utils;
080    import org.openstreetmap.josm.tools.XmlObjectParser;
081    import org.openstreetmap.josm.tools.template_engine.ParseError;
082    import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
083    import org.openstreetmap.josm.tools.template_engine.TemplateParser;
084    import org.xml.sax.SAXException;
085    
086    /**
087     * This class read encapsulate one tagging preset. A class method can
088     * read in all predefined presets, either shipped with JOSM or that are
089     * in the config directory.
090     *
091     * It is also able to construct dialogs out of preset definitions.
092     */
093    public class TaggingPreset extends AbstractAction implements MapView.LayerChangeListener {
094    
095        public enum PresetType {
096            NODE(/* ICON */"Mf_node", "node"),
097            WAY(/* ICON */"Mf_way", "way"),
098            RELATION(/* ICON */"Mf_relation", "relation"),
099            CLOSEDWAY(/* ICON */"Mf_closedway", "closedway");
100    
101            private final String iconName;
102            private final String name;
103    
104            PresetType(String iconName, String name) {
105                this.iconName = iconName;
106                this.name = name;
107            }
108    
109            public String getIconName() {
110                return iconName;
111            }
112    
113            public String getName() {
114                return name;
115            }
116    
117            public static PresetType forPrimitive(OsmPrimitive p) {
118                return forPrimitiveType(p.getDisplayType());
119            }
120    
121            public static PresetType forPrimitiveType(org.openstreetmap.josm.data.osm.OsmPrimitiveType type) {
122                switch (type) {
123                case NODE:
124                    return NODE;
125                case WAY:
126                    return WAY;
127                case CLOSEDWAY:
128                    return CLOSEDWAY;
129                case RELATION:
130                case MULTIPOLYGON:
131                    return RELATION;
132                default:
133                    throw new IllegalArgumentException("Unexpected primitive type: " + type);
134                }
135            }
136    
137            public static PresetType fromString(String type) {
138                for (PresetType t : PresetType.values()) {
139                    if (t.getName().equals(type))
140                        return t;
141                }
142                return null;
143            }
144        }
145    
146        /**
147         * Enum denoting how a match (see {@link Item#matches}) is performed.
148         */
149        private enum MatchType {
150    
151            /**
152             * Neutral, i.e., do not consider this item for matching.
153             */
154            NONE("none"),
155            /**
156             * Positive if key matches, neutral otherwise.
157             */
158            KEY("key"),
159            /**
160             * Positive if key matches, negative otherwise.
161             */
162            KEY_REQUIRED("key!"),
163            /**
164             * Positive if key and value matches, negative otherwise.
165             */
166            KEY_VALUE("keyvalue");
167    
168            private final String value;
169    
170            private MatchType(String value) {
171                this.value = value;
172            }
173    
174            public String getValue() {
175                return value;
176            }
177    
178            public static MatchType ofString(String type) {
179                for (MatchType i : EnumSet.allOf(MatchType.class)) {
180                    if (i.getValue().equals(type))
181                        return i;
182                }
183                throw new IllegalArgumentException(type + " is not allowed");
184            }
185        }
186    
187        public static final int DIALOG_ANSWER_APPLY = 1;
188        public static final int DIALOG_ANSWER_NEW_RELATION = 2;
189        public static final int DIALOG_ANSWER_CANCEL = 3;
190    
191        public TaggingPresetMenu group = null;
192        public String name;
193        public String name_context;
194        public String locale_name;
195        public final static String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text";
196        private static File zipIcons = null;
197        private static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
198    
199        public static abstract class Item {
200    
201            protected void initAutoCompletionField(AutoCompletingTextField field, String key) {
202                OsmDataLayer layer = Main.main.getEditLayer();
203                if (layer == null)
204                    return;
205                AutoCompletionList list = new AutoCompletionList();
206                Main.main.getEditLayer().data.getAutoCompletionManager().populateWithTagValues(list, key);
207                field.setAutoCompletionList(list);
208            }
209    
210            abstract boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel);
211    
212            abstract void addCommands(List<Tag> changedTags);
213    
214            boolean requestFocusInWindow() {
215                return false;
216            }
217    
218            /**
219             * Tests whether the tags match this item.
220             * Note that for a match, at least one positive and no negative is required.
221             * @param tags the tags of an {@link OsmPrimitive}
222             * @return {@code true} if matches (positive), {@code null} if neutral, {@code false} if mismatches (negative).
223             */
224            Boolean matches(Map<String, String> tags) {
225                return null;
226            }
227        }
228    
229        public static abstract class KeyedItem extends Item {
230    
231            public String key;
232            public String text;
233            public String text_context;
234            public String match = getDefaultMatch().getValue();
235    
236            public abstract MatchType getDefaultMatch();
237            public abstract Collection<String> getValues();
238    
239            @Override
240            Boolean matches(Map<String, String> tags) {
241                switch (MatchType.ofString(match)) {
242                case NONE:
243                    return null;
244                case KEY:
245                    return tags.containsKey(key) ? true : null;
246                case KEY_REQUIRED:
247                    return tags.containsKey(key);
248                case KEY_VALUE:
249                    return tags.containsKey(key) && (getValues().contains(tags.get(key)));
250                default:
251                    throw new IllegalStateException();
252                }
253            }
254    
255        }
256    
257        public static class Usage {
258            TreeSet<String> values;
259            boolean hadKeys = false;
260            boolean hadEmpty = false;
261            public boolean hasUniqueValue() {
262                return values.size() == 1 && !hadEmpty;
263            }
264    
265            public boolean unused() {
266                return values.isEmpty();
267            }
268            public String getFirst() {
269                return values.first();
270            }
271    
272            public boolean hadKeys() {
273                return hadKeys;
274            }
275        }
276    
277        public static final String DIFFERENT = tr("<different>");
278    
279        static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
280            Usage returnValue = new Usage();
281            returnValue.values = new TreeSet<String>();
282            for (OsmPrimitive s : sel) {
283                String v = s.get(key);
284                if (v != null) {
285                    returnValue.values.add(v);
286                } else {
287                    returnValue.hadEmpty = true;
288                }
289                if(s.hasKeys()) {
290                    returnValue.hadKeys = true;
291                }
292            }
293            return returnValue;
294        }
295    
296        static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
297    
298            Usage returnValue = new Usage();
299            returnValue.values = new TreeSet<String>();
300            for (OsmPrimitive s : sel) {
301                String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
302                if (booleanValue != null) {
303                    returnValue.values.add(booleanValue);
304                }
305            }
306            return returnValue;
307        }
308    
309        public static class PresetListEntry {
310            public String value;
311            public String value_context;
312            public String display_value;
313            public String short_description;
314            public String icon;
315            public String locale_display_value;
316            public String locale_short_description;
317            private final File zipIcons = TaggingPreset.zipIcons;
318    
319            // Cached size (currently only for Combo) to speed up preset dialog initialization
320            private int prefferedWidth = -1;
321            private int prefferedHeight = -1;
322    
323            public String getListDisplay() {
324                if (value.equals(DIFFERENT))
325                    return "<b>"+DIFFERENT.replaceAll("<", "&lt;").replaceAll(">", "&gt;")+"</b>";
326    
327                if (value.equals(""))
328                    return "&nbsp;";
329    
330                final StringBuilder res = new StringBuilder("<b>");
331                res.append(getDisplayValue(true));
332                res.append("</b>");
333                if (getShortDescription(true) != null) {
334                    // wrap in table to restrict the text width
335                    res.append("<div style=\"width:300px; padding:0 0 5px 5px\">");
336                    res.append(getShortDescription(true));
337                    res.append("</div>");
338                }
339                return res.toString();
340            }
341    
342            public ImageIcon getIcon() {
343                return icon == null ? null : loadImageIcon(icon, zipIcons, 24);
344            }
345    
346            public PresetListEntry() {
347            }
348    
349            public PresetListEntry(String value) {
350                this.value = value;
351            }
352    
353            public String getDisplayValue(boolean translated) {
354                return translated
355                        ? Utils.firstNonNull(locale_display_value, tr(display_value), trc(value_context, value))
356                                : Utils.firstNonNull(display_value, value);
357            }
358    
359            public String getShortDescription(boolean translated) {
360                return translated
361                        ? Utils.firstNonNull(locale_short_description, tr(short_description))
362                                : short_description;
363            }
364    
365            // toString is mainly used to initialize the Editor
366            @Override
367            public String toString() {
368                if (value.equals(DIFFERENT))
369                    return DIFFERENT;
370                return getDisplayValue(true).replaceAll("<.*>", ""); // remove additional markup, e.g. <br>
371            }
372        }
373    
374        public static class Text extends KeyedItem {
375    
376            public String locale_text;
377            public String default_;
378            public String originalValue;
379            public String use_last_as_default = "false";
380    
381            private JComponent value;
382    
383            @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
384    
385                // find out if our key is already used in the selection.
386                Usage usage = determineTextUsage(sel, key);
387                AutoCompletingTextField textField = new AutoCompletingTextField();
388                initAutoCompletionField(textField, key);
389                if (usage.unused()){
390                    if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
391                        // selected osm primitives are untagged or filling default values feature is enabled
392                        if (!"false".equals(use_last_as_default) && lastValue.containsKey(key)) {
393                            textField.setText(lastValue.get(key));
394                        } else {
395                            textField.setText(default_);
396                        }
397                    } else {
398                        // selected osm primitives are tagged and filling default values feature is disabled
399                        textField.setText("");
400                    }
401                    value = textField;
402                    originalValue = null;
403                } else if (usage.hasUniqueValue()) {
404                    // all objects use the same value
405                    textField.setText(usage.getFirst());
406                    value = textField;
407                    originalValue = usage.getFirst();
408                } else {
409                    // the objects have different values
410                    JosmComboBox comboBox = new JosmComboBox(usage.values.toArray());
411                    comboBox.setEditable(true);
412                    comboBox.setEditor(textField);
413                    comboBox.getEditor().setItem(DIFFERENT);
414                    value=comboBox;
415                    originalValue = DIFFERENT;
416                }
417                if(locale_text == null) {
418                    if (text != null) {
419                        if(text_context != null) {
420                            locale_text = trc(text_context, fixPresetString(text));
421                        } else {
422                            locale_text = tr(fixPresetString(text));
423                        }
424                    }
425                }
426                p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
427                p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
428                return true;
429            }
430    
431            @Override
432            public void addCommands(List<Tag> changedTags) {
433    
434                // return if unchanged
435                String v = (value instanceof JosmComboBox)
436                        ? ((JosmComboBox) value).getEditor().getItem().toString()
437                                : ((JTextField) value).getText();
438                        v = v.trim();
439    
440                        if (!"false".equals(use_last_as_default)) {
441                            lastValue.put(key, v);
442                        }
443                        if (v.equals(originalValue) || (originalValue == null && v.length() == 0))
444                            return;
445    
446                        changedTags.add(new Tag(key, v));
447            }
448    
449            @Override
450            boolean requestFocusInWindow() {
451                return value.requestFocusInWindow();
452            }
453    
454            @Override
455            public MatchType getDefaultMatch() {
456                return MatchType.NONE;
457            }
458    
459            @Override
460            public Collection<String> getValues() {
461                if (default_ == null || default_.isEmpty())
462                    return Collections.emptyList();
463                return Collections.singleton(default_);
464            }
465        }
466    
467        public static class Check extends KeyedItem {
468    
469            public String locale_text;
470            public String value_on = OsmUtils.trueval;
471            public String value_off = OsmUtils.falseval;
472            public boolean default_ = false; // only used for tagless objects
473    
474            private QuadStateCheckBox check;
475            private QuadStateCheckBox.State initialState;
476            private boolean def;
477    
478            @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
479    
480                // find out if our key is already used in the selection.
481                Usage usage = determineBooleanUsage(sel, key);
482                def = default_;
483    
484                if(locale_text == null) {
485                    if(text_context != null) {
486                        locale_text = trc(text_context, fixPresetString(text));
487                    } else {
488                        locale_text = tr(fixPresetString(text));
489                    }
490                }
491    
492                String oneValue = null;
493                for (String s : usage.values) {
494                    oneValue = s;
495                }
496                if (usage.values.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) {
497                    if (def && !PROP_FILL_DEFAULT.get()) {
498                        // default is set and filling default values feature is disabled - check if all primitives are untagged
499                        for (OsmPrimitive s : sel)
500                            if(s.hasKeys()) {
501                                def = false;
502                            }
503                    }
504    
505                    // all selected objects share the same value which is either true or false or unset,
506                    // we can display a standard check box.
507                    initialState = value_on.equals(oneValue) ?
508                            QuadStateCheckBox.State.SELECTED :
509                                value_off.equals(oneValue) ?
510                                        QuadStateCheckBox.State.NOT_SELECTED :
511                                            def ? QuadStateCheckBox.State.SELECTED
512                                                    : QuadStateCheckBox.State.UNSET;
513                    check = new QuadStateCheckBox(locale_text, initialState,
514                            new QuadStateCheckBox.State[] {
515                            QuadStateCheckBox.State.SELECTED,
516                            QuadStateCheckBox.State.NOT_SELECTED,
517                            QuadStateCheckBox.State.UNSET });
518                } else {
519                    def = false;
520                    // the objects have different values, or one or more objects have something
521                    // else than true/false. we display a quad-state check box
522                    // in "partial" state.
523                    initialState = QuadStateCheckBox.State.PARTIAL;
524                    check = new QuadStateCheckBox(locale_text, QuadStateCheckBox.State.PARTIAL,
525                            new QuadStateCheckBox.State[] {
526                            QuadStateCheckBox.State.PARTIAL,
527                            QuadStateCheckBox.State.SELECTED,
528                            QuadStateCheckBox.State.NOT_SELECTED,
529                            QuadStateCheckBox.State.UNSET });
530                }
531                p.add(check, GBC.eol().fill(GBC.HORIZONTAL));
532                return true;
533            }
534    
535            @Override public void addCommands(List<Tag> changedTags) {
536                // if the user hasn't changed anything, don't create a command.
537                if (check.getState() == initialState && !def) return;
538    
539                // otherwise change things according to the selected value.
540                changedTags.add(new Tag(key,
541                        check.getState() == QuadStateCheckBox.State.SELECTED ? value_on :
542                            check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? value_off :
543                                null));
544            }
545            @Override boolean requestFocusInWindow() {return check.requestFocusInWindow();}
546    
547            @Override
548            public MatchType getDefaultMatch() {
549                return MatchType.NONE;
550            }
551    
552            @Override
553            public Collection<String> getValues() {
554                return Arrays.asList(value_on, value_off);
555            }
556        }
557    
558        public static abstract class ComboMultiSelect extends KeyedItem {
559    
560            public String locale_text;
561            public String values;
562            public String values_context;
563            public String display_values;
564            public String locale_display_values;
565            public String short_descriptions;
566            public String locale_short_descriptions;
567            public String default_;
568            public String delimiter = ";";
569            public String use_last_as_default = "false";
570    
571            protected JComponent component;
572            protected Map<String, PresetListEntry> lhm = new LinkedHashMap<String, PresetListEntry>();
573            private boolean initialized = false;
574            protected Usage usage;
575            protected Object originalValue;
576    
577            protected abstract Object getSelectedItem();
578            protected abstract void addToPanelAnchor(JPanel p, String def);
579    
580            protected char getDelChar() {
581                return delimiter.isEmpty() ? ';' : delimiter.charAt(0);
582            }
583    
584            @Override
585            public Collection<String> getValues() {
586                initListEntries();
587                return lhm.keySet();
588            }
589    
590            public Collection<String> getDisplayValues() {
591                initListEntries();
592                return Utils.transform(lhm.values(), new Utils.Function<PresetListEntry, String>() {
593    
594                    @Override
595                    public String apply(PresetListEntry x) {
596                        return x.getDisplayValue(true);
597                    }
598                });
599            }
600    
601            @Override
602            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
603    
604                initListEntries();
605    
606                // find out if our key is already used in the selection.
607                usage = determineTextUsage(sel, key);
608                if (!usage.hasUniqueValue() && !usage.unused()) {
609                    lhm.put(DIFFERENT, new PresetListEntry(DIFFERENT));
610                }
611    
612                p.add(new JLabel(tr("{0}:", locale_text)), GBC.std().insets(0, 0, 10, 0));
613                addToPanelAnchor(p, default_);
614    
615                return true;
616    
617            }
618    
619            private void initListEntries() {
620                if (initialized) {
621                    lhm.remove(DIFFERENT); // possibly added in #addToPanel
622                    return;
623                } else if (lhm.isEmpty()) {
624                    initListEntriesFromAttributes();
625                } else {
626                    if (values != null) {
627                        System.err.println(tr("Warning in tagging preset \"{0}-{1}\": "
628                                + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
629                                key, text, "values", "list_entry"));
630                    }
631                    if (display_values != null || locale_display_values != null) {
632                        System.err.println(tr("Warning in tagging preset \"{0}-{1}\": "
633                                + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
634                                key, text, "display_values", "list_entry"));
635                    }
636                    if (short_descriptions != null || locale_short_descriptions != null) {
637                        System.err.println(tr("Warning in tagging preset \"{0}-{1}\": "
638                                + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
639                                key, text, "short_descriptions", "list_entry"));
640                    }
641                    for (PresetListEntry e : lhm.values()) {
642                        if (e.value_context == null) {
643                            e.value_context = values_context;
644                        }
645                    }
646                }
647                if (locale_text == null) {
648                    locale_text = trc(text_context, fixPresetString(text));
649                }
650                initialized = true;
651            }
652    
653            private String[] initListEntriesFromAttributes() {
654                char delChar = getDelChar();
655    
656                String[] value_array = splitEscaped(delChar, values);
657    
658                final String displ = Utils.firstNonNull(locale_display_values, display_values);
659                String[] display_array = displ == null ? value_array : splitEscaped(delChar, displ);
660    
661                final String descr = Utils.firstNonNull(locale_short_descriptions, short_descriptions);
662                String[] short_descriptions_array = descr == null ? null : splitEscaped(delChar, descr);
663    
664                if (display_array.length != value_array.length) {
665                    System.err.println(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''display_values'' must be the same as in ''values''", key, text));
666                    display_array = value_array;
667                }
668    
669                if (short_descriptions_array != null && short_descriptions_array.length != value_array.length) {
670                    System.err.println(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''short_descriptions'' must be the same as in ''values''", key, text));
671                    short_descriptions_array = null;
672                }
673    
674                for (int i = 0; i < value_array.length; i++) {
675                    final PresetListEntry e = new PresetListEntry(value_array[i]);
676                    e.locale_display_value = locale_display_values != null
677                            ? display_array[i]
678                                    : trc(values_context, fixPresetString(display_array[i]));
679                            if (short_descriptions_array != null) {
680                                e.locale_short_description = locale_short_descriptions != null
681                                        ? short_descriptions_array[i]
682                                                : tr(fixPresetString(short_descriptions_array[i]));
683                            }
684                            lhm.put(value_array[i], e);
685                            display_array[i] = e.getDisplayValue(true);
686                }
687    
688                return display_array;
689            }
690    
691            protected String getDisplayIfNull(String display) {
692                return display;
693            }
694    
695            @Override
696            public void addCommands(List<Tag> changedTags) {
697                Object obj = getSelectedItem();
698                String display = (obj == null) ? null : obj.toString();
699                String value = null;
700                if (display == null) {
701                    display = getDisplayIfNull(display);
702                }
703    
704                if (display != null) {
705                    for (String key : lhm.keySet()) {
706                        String k = lhm.get(key).toString();
707                        if (k != null && k.equals(display)) {
708                            value = key;
709                            break;
710                        }
711                    }
712                    if (value == null) {
713                        value = display;
714                    }
715                } else {
716                    value = "";
717                }
718                value = value.trim();
719    
720                // no change if same as before
721                if (originalValue == null) {
722                    if (value.length() == 0)
723                        return;
724                } else if (value.equals(originalValue.toString()))
725                    return;
726    
727                if (!"false".equals(use_last_as_default)) {
728                    lastValue.put(key, value);
729                }
730                changedTags.add(new Tag(key, value));
731            }
732    
733            public void addListEntry(PresetListEntry e) {
734                lhm.put(e.value, e);
735            }
736    
737            public void addListEntries(Collection<PresetListEntry> e) {
738                for (PresetListEntry i : e) {
739                    addListEntry(i);
740                }
741            }
742    
743            @Override
744            boolean requestFocusInWindow() {
745                return component.requestFocusInWindow();
746            }
747    
748            private static ListCellRenderer RENDERER = new ListCellRenderer() {
749    
750                JLabel lbl = new JLabel();
751    
752                public Component getListCellRendererComponent(
753                        JList list,
754                        Object value,
755                        int index,
756                        boolean isSelected,
757                        boolean cellHasFocus) {
758                    PresetListEntry item = (PresetListEntry) value;
759                    
760                    // Only return cached size, item is not shown
761                    if (!list.isShowing() && item.prefferedWidth != -1 && item.prefferedHeight != -1) {
762                        if (index == -1) {
763                            lbl.setPreferredSize(new Dimension(item.prefferedWidth, 10));
764                        } else {
765                            lbl.setPreferredSize(new Dimension(item.prefferedWidth, item.prefferedHeight));
766                        }
767                        return lbl;
768                    }
769    
770                    lbl.setPreferredSize(null);
771    
772    
773                    if (isSelected) {
774                        lbl.setBackground(list.getSelectionBackground());
775                        lbl.setForeground(list.getSelectionForeground());
776                    } else {
777                        lbl.setBackground(list.getBackground());
778                        lbl.setForeground(list.getForeground());
779                    }
780    
781                    lbl.setOpaque(true);
782                    lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
783                    lbl.setText("<html>" + item.getListDisplay() + "</html>");
784                    lbl.setIcon(item.getIcon());
785                    lbl.setEnabled(list.isEnabled());
786    
787                    // Cache size
788                    item.prefferedWidth = lbl.getPreferredSize().width;
789                    item.prefferedHeight = lbl.getPreferredSize().height;
790    
791                    // We do not want the editor to have the maximum height of all
792                    // entries. Return a dummy with bogus height.
793                    if (index == -1) {
794                        lbl.setPreferredSize(new Dimension(lbl.getPreferredSize().width, 10));
795                    }
796                    return lbl;
797                }
798            };
799    
800    
801            protected ListCellRenderer getListCellRenderer() {
802                return RENDERER;
803            }
804    
805            @Override
806            public MatchType getDefaultMatch() {
807                return MatchType.NONE;
808            }
809        }
810    
811        public static class Combo extends ComboMultiSelect {
812    
813            public boolean editable = true;
814            protected JosmComboBox combo;
815    
816            public Combo() {
817                delimiter = ",";
818            }
819    
820            @Override
821            protected void addToPanelAnchor(JPanel p, String def) {
822                if (!usage.unused()) {
823                    for (String s : usage.values) {
824                        if (!lhm.containsKey(s)) {
825                            lhm.put(s, new PresetListEntry(s));
826                        }
827                    }
828                }
829                if (def != null && !lhm.containsKey(def)) {
830                    lhm.put(def, new PresetListEntry(def));
831                }
832                lhm.put("", new PresetListEntry(""));
833    
834                combo = new JosmComboBox(lhm.values().toArray());
835                component = combo;
836                combo.setRenderer(getListCellRenderer());
837                combo.setEditable(editable);
838                combo.reinitialize(lhm.values());
839                AutoCompletingTextField tf = new AutoCompletingTextField();
840                initAutoCompletionField(tf, key);
841                AutoCompletionList acList = tf.getAutoCompletionList();
842                if (acList != null) {
843                    acList.add(getDisplayValues(), AutoCompletionItemPritority.IS_IN_STANDARD);
844                }
845                combo.setEditor(tf);
846    
847                if (usage.hasUniqueValue()) {
848                    // all items have the same value (and there were no unset items)
849                    originalValue = lhm.get(usage.getFirst());
850                    combo.setSelectedItem(originalValue);
851                } else if (def != null && usage.unused()) {
852                    // default is set and all items were unset
853                    if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
854                        // selected osm primitives are untagged or filling default feature is enabled
855                        combo.setSelectedItem(lhm.get(def).getDisplayValue(true));
856                    } else {
857                        // selected osm primitives are tagged and filling default feature is disabled
858                        combo.setSelectedItem("");
859                    }
860                    originalValue = lhm.get(DIFFERENT);
861                } else if (usage.unused()) {
862                    // all items were unset (and so is default)
863                    originalValue = lhm.get("");
864                    if ("force".equals(use_last_as_default) && lastValue.containsKey(key)) {
865                        combo.setSelectedItem(lhm.get(lastValue.get(key)));
866                    } else {
867                        combo.setSelectedItem(originalValue);
868                    }
869                } else {
870                    originalValue = lhm.get(DIFFERENT);
871                    combo.setSelectedItem(originalValue);
872                }
873                p.add(combo, GBC.eol().fill(GBC.HORIZONTAL));
874    
875            }
876    
877            @Override
878            protected Object getSelectedItem() {
879                return combo.getSelectedItem();
880    
881            }
882    
883            @Override
884            protected String getDisplayIfNull(String display) {
885                if (combo.isEditable())
886                    return combo.getEditor().getItem().toString();
887                else
888                    return display;
889    
890            }
891        }
892    
893        /**
894         * Class that allows list values to be assigned and retrieved as a comma-delimited
895         * string.
896         */
897        public static class ConcatenatingJList extends JList {
898            private String delimiter;
899            public ConcatenatingJList(String del, Object[] o) {
900                super(o);
901                delimiter = del;
902            }
903            public void setSelectedItem(Object o) {
904                if (o == null) {
905                    clearSelection();
906                } else {
907                    String s = o.toString();
908                    HashSet<String> parts = new HashSet<String>(Arrays.asList(s.split(delimiter)));
909                    ListModel lm = getModel();
910                    int[] intParts = new int[lm.getSize()];
911                    int j = 0;
912                    for (int i = 0; i < lm.getSize(); i++) {
913                        if (parts.contains((((PresetListEntry)lm.getElementAt(i)).value))) {
914                            intParts[j++]=i;
915                        }
916                    }
917                    setSelectedIndices(Arrays.copyOf(intParts, j));
918                    // check if we have actually managed to represent the full
919                    // value with our presets. if not, cop out; we will not offer
920                    // a selection list that threatens to ruin the value.
921                    setEnabled(s.equals(getSelectedItem()));
922                }
923            }
924            public String getSelectedItem() {
925                ListModel lm = getModel();
926                int[] si = getSelectedIndices();
927                StringBuilder builder = new StringBuilder();
928                for (int i=0; i<si.length; i++) {
929                    if (i>0) {
930                        builder.append(delimiter);
931                    }
932                    builder.append(((PresetListEntry)lm.getElementAt(si[i])).value);
933                }
934                return builder.toString();
935            }
936        }
937    
938        public static class MultiSelect extends ComboMultiSelect {
939    
940            public long rows = -1;
941            protected ConcatenatingJList list;
942    
943            @Override
944            protected void addToPanelAnchor(JPanel p, String def) {
945                list = new ConcatenatingJList(delimiter, lhm.values().toArray());
946                component = list;
947                ListCellRenderer renderer = getListCellRenderer();
948                list.setCellRenderer(renderer);
949    
950                if (usage.hasUniqueValue() && !usage.unused()) {
951                    originalValue = usage.getFirst();
952                    list.setSelectedItem(originalValue);
953                } else if (def != null && !usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
954                    originalValue = DIFFERENT;
955                    list.setSelectedItem(def);
956                } else if (usage.unused()) {
957                    originalValue = null;
958                    list.setSelectedItem(originalValue);
959                } else {
960                    originalValue = DIFFERENT;
961                    list.setSelectedItem(originalValue);
962                }
963    
964                JScrollPane sp = new JScrollPane(list);
965                // if a number of rows has been specified in the preset,
966                // modify preferred height of scroll pane to match that row count.
967                if (rows != -1) {
968                    double height = renderer.getListCellRendererComponent(list,
969                            new PresetListEntry("x"), 0, false, false).getPreferredSize().getHeight() * rows;
970                    sp.setPreferredSize(new Dimension((int) sp.getPreferredSize().getWidth(), (int) height));
971                }
972                p.add(sp, GBC.eol().fill(GBC.HORIZONTAL));
973    
974    
975            }
976    
977            @Override
978            protected Object getSelectedItem() {
979                return list.getSelectedItem();
980            }
981        }
982    
983        /**
984         * allow escaped comma in comma separated list:
985         * "A\, B\, C,one\, two" --> ["A, B, C", "one, two"]
986         * @param delimiter the delimiter, e.g. a comma. separates the entries and
987         *      must be escaped within one entry
988         * @param s the string
989         */
990        private static String[] splitEscaped(char delimiter, String s) {
991            if (s == null)
992                return new String[0];
993            List<String> result = new ArrayList<String>();
994            boolean backslash = false;
995            StringBuilder item = new StringBuilder();
996            for (int i=0; i<s.length(); i++) {
997                char ch = s.charAt(i);
998                if (backslash) {
999                    item.append(ch);
1000                    backslash = false;
1001                } else if (ch == '\\') {
1002                    backslash = true;
1003                } else if (ch == delimiter) {
1004                    result.add(item.toString());
1005                    item.setLength(0);
1006                } else {
1007                    item.append(ch);
1008                }
1009            }
1010            if (item.length() > 0) {
1011                result.add(item.toString());
1012            }
1013            return result.toArray(new String[result.size()]);
1014        }
1015    
1016        public static class Label extends Item {
1017    
1018            public String text;
1019            public String text_context;
1020            public String locale_text;
1021    
1022            @Override
1023            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
1024                if (locale_text == null) {
1025                    if (text_context != null) {
1026                        locale_text = trc(text_context, fixPresetString(text));
1027                    } else {
1028                        locale_text = tr(fixPresetString(text));
1029                    }
1030                }
1031                p.add(new JLabel(locale_text), GBC.eol());
1032                return false;
1033            }
1034    
1035            @Override
1036            public void addCommands(List<Tag> changedTags) {
1037            }
1038        }
1039    
1040        public static class Link extends Item {
1041    
1042            public String href;
1043            public String text;
1044            public String text_context;
1045            public String locale_text;
1046            public String locale_href;
1047    
1048            @Override
1049            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
1050                if (locale_text == null) {
1051                    if (text == null) {
1052                        locale_text = tr("More information about this feature");
1053                    } else if (text_context != null) {
1054                        locale_text = trc(text_context, fixPresetString(text));
1055                    } else {
1056                        locale_text = tr(fixPresetString(text));
1057                    }
1058                }
1059                String url = locale_href;
1060                if (url == null) {
1061                    url = href;
1062                }
1063                if (url != null) {
1064                    p.add(new UrlLabel(url, locale_text, 2), GBC.eol().anchor(GBC.WEST));
1065                }
1066                return false;
1067            }
1068    
1069            @Override
1070            public void addCommands(List<Tag> changedTags) {
1071            }
1072        }
1073    
1074        public static class Role {
1075            public EnumSet<PresetType> types;
1076            public String key;
1077            public String text;
1078            public String text_context;
1079            public String locale_text;
1080    
1081            public boolean required = false;
1082            public long count = 0;
1083    
1084            public void setType(String types) throws SAXException {
1085                this.types = TaggingPreset.getType(types);
1086            }
1087    
1088            public void setRequisite(String str) throws SAXException {
1089                if("required".equals(str)) {
1090                    required = true;
1091                } else if(!"optional".equals(str))
1092                    throw new SAXException(tr("Unknown requisite: {0}", str));
1093            }
1094    
1095            /* return either argument, the highest possible value or the lowest
1096               allowed value */
1097            public long getValidCount(long c)
1098            {
1099                if(count > 0 && !required)
1100                    return c != 0 ? count : 0;
1101                else if(count > 0)
1102                    return count;
1103                else if(!required)
1104                    return c != 0  ? c : 0;
1105                else
1106                    return c != 0  ? c : 1;
1107            }
1108            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
1109                String cstring;
1110                if(count > 0 && !required) {
1111                    cstring = "0,"+String.valueOf(count);
1112                } else if(count > 0) {
1113                    cstring = String.valueOf(count);
1114                } else if(!required) {
1115                    cstring = "0-...";
1116                } else {
1117                    cstring = "1-...";
1118                }
1119                if(locale_text == null) {
1120                    if (text != null) {
1121                        if(text_context != null) {
1122                            locale_text = trc(text_context, fixPresetString(text));
1123                        } else {
1124                            locale_text = tr(fixPresetString(text));
1125                        }
1126                    }
1127                }
1128                p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
1129                p.add(new JLabel(key), GBC.std().insets(0,0,10,0));
1130                p.add(new JLabel(cstring), types == null ? GBC.eol() : GBC.std().insets(0,0,10,0));
1131                if(types != null){
1132                    JPanel pp = new JPanel();
1133                    for(PresetType t : types) {
1134                        pp.add(new JLabel(ImageProvider.get(t.getIconName())));
1135                    }
1136                    p.add(pp, GBC.eol());
1137                }
1138                return true;
1139            }
1140        }
1141    
1142        public static class Roles extends Item {
1143    
1144            public List<Role> roles = new LinkedList<Role>();
1145    
1146            @Override
1147            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
1148                p.add(new JLabel(" "), GBC.eol()); // space
1149                if (roles.size() > 0) {
1150                    JPanel proles = new JPanel(new GridBagLayout());
1151                    proles.add(new JLabel(tr("Available roles")), GBC.std().insets(0, 0, 10, 0));
1152                    proles.add(new JLabel(tr("role")), GBC.std().insets(0, 0, 10, 0));
1153                    proles.add(new JLabel(tr("count")), GBC.std().insets(0, 0, 10, 0));
1154                    proles.add(new JLabel(tr("elements")), GBC.eol());
1155                    for (Role i : roles) {
1156                        i.addToPanel(proles, sel);
1157                    }
1158                    p.add(proles, GBC.eol());
1159                }
1160                return false;
1161            }
1162    
1163            @Override
1164            public void addCommands(List<Tag> changedTags) {
1165            }
1166        }
1167    
1168        public static class Optional extends Item {
1169    
1170            // TODO: Draw a box around optional stuff
1171            @Override
1172            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
1173                p.add(new JLabel(" "), GBC.eol()); // space
1174                p.add(new JLabel(tr("Optional Attributes:")), GBC.eol());
1175                p.add(new JLabel(" "), GBC.eol()); // space
1176                return false;
1177            }
1178    
1179            @Override
1180            public void addCommands(List<Tag> changedTags) {
1181            }
1182        }
1183    
1184        public static class Space extends Item {
1185    
1186            @Override
1187            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
1188                p.add(new JLabel(" "), GBC.eol()); // space
1189                return false;
1190            }
1191    
1192            @Override
1193            public void addCommands(List<Tag> changedTags) {
1194            }
1195        }
1196    
1197        public static class Key extends KeyedItem {
1198    
1199            public String value;
1200    
1201            @Override
1202            public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
1203                return false;
1204            }
1205    
1206            @Override
1207            public void addCommands(List<Tag> changedTags) {
1208                changedTags.add(new Tag(key, value));
1209            }
1210    
1211            @Override
1212            public MatchType getDefaultMatch() {
1213                return MatchType.KEY_VALUE;
1214            }
1215    
1216            @Override
1217            public Collection<String> getValues() {
1218                return Collections.singleton(value);
1219            }
1220        }
1221    
1222        /**
1223         * The types as preparsed collection.
1224         */
1225        public EnumSet<PresetType> types;
1226        public List<Item> data = new LinkedList<Item>();
1227        public TemplateEntry nameTemplate;
1228        public Match nameTemplateFilter;
1229        private static final HashMap<String,String> lastValue = new HashMap<String,String>();
1230    
1231        /**
1232         * Create an empty tagging preset. This will not have any items and
1233         * will be an empty string as text. createPanel will return null.
1234         * Use this as default item for "do not select anything".
1235         */
1236        public TaggingPreset() {
1237            MapView.addLayerChangeListener(this);
1238            updateEnabledState();
1239        }
1240    
1241        /**
1242         * Change the display name without changing the toolbar value.
1243         */
1244        public void setDisplayName() {
1245            putValue(Action.NAME, getName());
1246            putValue("toolbar", "tagging_" + getRawName());
1247            putValue(OPTIONAL_TOOLTIP_TEXT, (group != null ?
1248                    tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) :
1249                        tr("Use preset ''{0}''", getLocaleName())));
1250        }
1251    
1252        public String getLocaleName() {
1253            if(locale_name == null) {
1254                if(name_context != null) {
1255                    locale_name = trc(name_context, fixPresetString(name));
1256                } else {
1257                    locale_name = tr(fixPresetString(name));
1258                }
1259            }
1260            return locale_name;
1261        }
1262    
1263        public String getName() {
1264            return group != null ? group.getName() + "/" + getLocaleName() : getLocaleName();
1265        }
1266        public String getRawName() {
1267            return group != null ? group.getRawName() + "/" + name : name;
1268        }
1269    
1270        protected static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) {
1271            final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null);
1272            ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true);
1273            if (maxSize != null) {
1274                imgProv.setMaxSize(24);
1275            }
1276            return imgProv.get();
1277        }
1278    
1279        /*
1280         * Called from the XML parser to set the icon.
1281         * This task is performed in the background in order to speedup startup.
1282         *
1283         * FIXME for Java 1.6 - use 24x24 icons for LARGE_ICON_KEY (button bar)
1284         * and the 16x16 icons for SMALL_ICON.
1285         */
1286        public void setIcon(final String iconName) {
1287            ImageProvider imgProv = new ImageProvider(iconName);
1288            final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null);
1289            imgProv.setDirs(s);
1290            imgProv.setId("presets");
1291            imgProv.setArchive(TaggingPreset.zipIcons);
1292            imgProv.setOptional(true);
1293            imgProv.setMaxWidth(16).setMaxHeight(16);
1294            imgProv.getInBackground(new ImageProvider.ImageCallback() {
1295                @Override
1296                public void finished(final ImageIcon result) {
1297                    if (result != null) {
1298                        GuiHelper.runInEDT(new Runnable() {
1299                            @Override
1300                            public void run() {
1301                                putValue(Action.SMALL_ICON, result);
1302                            }
1303                        });
1304                    } else {
1305                        System.out.println("Could not get presets icon " + iconName);
1306                    }
1307                }
1308            });
1309        }
1310    
1311        // cache the parsing of types using a LRU cache (http://java-planet.blogspot.com/2005/08/how-to-set-up-simple-lru-cache-using.html)
1312        private static final Map<String,EnumSet<PresetType>> typeCache =
1313                new LinkedHashMap<String, EnumSet<PresetType>>(16, 1.1f, true);
1314    
1315        static public EnumSet<PresetType> getType(String types) throws SAXException {
1316            if (typeCache.containsKey(types))
1317                return typeCache.get(types);
1318            EnumSet<PresetType> result = EnumSet.noneOf(PresetType.class);
1319            for (String type : Arrays.asList(types.split(","))) {
1320                try {
1321                    PresetType presetType = PresetType.fromString(type);
1322                    result.add(presetType);
1323                } catch (IllegalArgumentException e) {
1324                    throw new SAXException(tr("Unknown type: {0}", type));
1325                }
1326            }
1327            typeCache.put(types, result);
1328            return result;
1329        }
1330    
1331        /*
1332         * Called from the XML parser to set the types this preset affects.
1333         */
1334        public void setType(String types) throws SAXException {
1335            this.types = getType(types);
1336        }
1337    
1338        public void setName_template(String pattern) throws SAXException {
1339            try {
1340                this.nameTemplate = new TemplateParser(pattern).parse();
1341            } catch (ParseError e) {
1342                System.err.println("Error while parsing " + pattern + ": " + e.getMessage());
1343                throw new SAXException(e);
1344            }
1345        }
1346    
1347        public void setName_template_filter(String filter) throws SAXException {
1348            try {
1349                this.nameTemplateFilter = SearchCompiler.compile(filter, false, false);
1350            } catch (org.openstreetmap.josm.actions.search.SearchCompiler.ParseError e) {
1351                System.err.println("Error while parsing" + filter + ": " + e.getMessage());
1352                throw new SAXException(e);
1353            }
1354        }
1355    
1356    
1357        public static List<TaggingPreset> readAll(Reader in, boolean validate) throws SAXException {
1358            XmlObjectParser parser = new XmlObjectParser();
1359            parser.mapOnStart("item", TaggingPreset.class);
1360            parser.mapOnStart("separator", TaggingPresetSeparator.class);
1361            parser.mapBoth("group", TaggingPresetMenu.class);
1362            parser.map("text", Text.class);
1363            parser.map("link", Link.class);
1364            parser.mapOnStart("optional", Optional.class);
1365            parser.mapOnStart("roles", Roles.class);
1366            parser.map("role", Role.class);
1367            parser.map("check", Check.class);
1368            parser.map("combo", Combo.class);
1369            parser.map("multiselect", MultiSelect.class);
1370            parser.map("label", Label.class);
1371            parser.map("space", Space.class);
1372            parser.map("key", Key.class);
1373            parser.map("list_entry", PresetListEntry.class);
1374            LinkedList<TaggingPreset> all = new LinkedList<TaggingPreset>();
1375            TaggingPresetMenu lastmenu = null;
1376            Roles lastrole = null;
1377            List<PresetListEntry> listEntries = new LinkedList<PresetListEntry>();
1378    
1379            if (validate) {
1380                parser.startWithValidation(in, "http://josm.openstreetmap.de/tagging-preset-1.0", "resource://data/tagging-preset.xsd");
1381            } else {
1382                parser.start(in);
1383            }
1384            while(parser.hasNext()) {
1385                Object o = parser.next();
1386                if (o instanceof TaggingPresetMenu) {
1387                    TaggingPresetMenu tp = (TaggingPresetMenu) o;
1388                    if(tp == lastmenu) {
1389                        lastmenu = tp.group;
1390                    } else
1391                    {
1392                        tp.group = lastmenu;
1393                        tp.setDisplayName();
1394                        lastmenu = tp;
1395                        all.add(tp);
1396    
1397                    }
1398                    lastrole = null;
1399                } else if (o instanceof TaggingPresetSeparator) {
1400                    TaggingPresetSeparator tp = (TaggingPresetSeparator) o;
1401                    tp.group = lastmenu;
1402                    all.add(tp);
1403                    lastrole = null;
1404                } else if (o instanceof TaggingPreset) {
1405                    TaggingPreset tp = (TaggingPreset) o;
1406                    tp.group = lastmenu;
1407                    tp.setDisplayName();
1408                    all.add(tp);
1409                    lastrole = null;
1410                } else {
1411                    if (all.size() != 0) {
1412                        if (o instanceof Roles) {
1413                            all.getLast().data.add((Item) o);
1414                            lastrole = (Roles) o;
1415                        } else if (o instanceof Role) {
1416                            if (lastrole == null)
1417                                throw new SAXException(tr("Preset role element without parent"));
1418                            lastrole.roles.add((Role) o);
1419                        } else if (o instanceof PresetListEntry) {
1420                            listEntries.add((PresetListEntry) o);
1421                        } else {
1422                            all.getLast().data.add((Item) o);
1423                            if (o instanceof ComboMultiSelect) {
1424                                ((ComboMultiSelect) o).addListEntries(listEntries);
1425                            }
1426                            listEntries = new LinkedList<PresetListEntry>();
1427                            lastrole = null;
1428                        }
1429                    } else
1430                        throw new SAXException(tr("Preset sub element without parent"));
1431                }
1432            }
1433            return all;
1434        }
1435    
1436        public static Collection<TaggingPreset> readAll(String source, boolean validate) throws SAXException, IOException {
1437            Collection<TaggingPreset> tp;
1438            MirroredInputStream s = new MirroredInputStream(source);
1439            try {
1440                InputStream zip = s.getZipEntry("xml","preset");
1441                if(zip != null) {
1442                    zipIcons = s.getFile();
1443                }
1444                InputStreamReader r;
1445                try {
1446                    r = new InputStreamReader(zip == null ? s : zip, "UTF-8");
1447                } catch (UnsupportedEncodingException e) {
1448                    r = new InputStreamReader(zip == null ? s: zip);
1449                }
1450                try {
1451                    tp = TaggingPreset.readAll(new BufferedReader(r), validate);
1452                } finally {
1453                    r.close();
1454                }
1455            } finally {
1456                s.close();
1457            }
1458            return tp;
1459        }
1460    
1461        public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate) {
1462            LinkedList<TaggingPreset> allPresets = new LinkedList<TaggingPreset>();
1463            for(String source : sources)  {
1464                try {
1465                    allPresets.addAll(TaggingPreset.readAll(source, validate));
1466                } catch (IOException e) {
1467                    e.printStackTrace();
1468                    JOptionPane.showMessageDialog(
1469                            Main.parent,
1470                            tr("Could not read tagging preset source: {0}",source),
1471                            tr("Error"),
1472                            JOptionPane.ERROR_MESSAGE
1473                            );
1474                } catch (SAXException e) {
1475                    System.err.println(e.getMessage());
1476                    System.err.println(source);
1477                    e.printStackTrace();
1478                    JOptionPane.showMessageDialog(
1479                            Main.parent,
1480                            tr("Error parsing {0}: ", source)+e.getMessage(),
1481                            tr("Error"),
1482                            JOptionPane.ERROR_MESSAGE
1483                            );
1484                }
1485            }
1486            return allPresets;
1487        }
1488    
1489        public static LinkedList<String> getPresetSources() {
1490            LinkedList<String> sources = new LinkedList<String>();
1491    
1492            for (SourceEntry e : (new PresetPrefHelper()).get()) {
1493                sources.add(e.url);
1494            }
1495    
1496            return sources;
1497        }
1498    
1499        public static Collection<TaggingPreset> readFromPreferences(boolean validate) {
1500            return readAll(getPresetSources(), validate);
1501        }
1502    
1503        private static class PresetPanel extends JPanel {
1504            boolean hasElements = false;
1505            PresetPanel()
1506            {
1507                super(new GridBagLayout());
1508            }
1509        }
1510    
1511        public PresetPanel createPanel(Collection<OsmPrimitive> selected) {
1512            if (data == null)
1513                return null;
1514            PresetPanel p = new PresetPanel();
1515            LinkedList<Item> l = new LinkedList<Item>();
1516            if(types != null){
1517                JPanel pp = new JPanel();
1518                for(PresetType t : types){
1519                    JLabel la = new JLabel(ImageProvider.get(t.getIconName()));
1520                    la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName())));
1521                    pp.add(la);
1522                }
1523                p.add(pp, GBC.eol());
1524            }
1525    
1526            JPanel items = new JPanel(new GridBagLayout());
1527            for (Item i : data){
1528                if(i instanceof Link) {
1529                    l.add(i);
1530                } else {
1531                    if(i.addToPanel(items, selected)) {
1532                        p.hasElements = true;
1533                    }
1534                }
1535            }
1536            p.add(items, GBC.eol().fill());
1537            if (selected.size() == 0 && !supportsRelation()) {
1538                GuiHelper.setEnabledRec(items, false);
1539            }
1540    
1541            for(Item link : l) {
1542                link.addToPanel(p, selected);
1543            }
1544    
1545            return p;
1546        }
1547    
1548        public boolean isShowable()
1549        {
1550            for(Item i : data)
1551            {
1552                if(!(i instanceof Optional || i instanceof Space || i instanceof Key))
1553                    return true;
1554            }
1555            return false;
1556        }
1557    
1558        public void actionPerformed(ActionEvent e) {
1559            if (Main.main == null) return;
1560            if (Main.main.getCurrentDataSet() == null) return;
1561    
1562            Collection<OsmPrimitive> sel = createSelection(Main.main.getCurrentDataSet().getSelected());
1563            int answer = showDialog(sel, supportsRelation());
1564    
1565            if (sel.size() != 0 && answer == DIALOG_ANSWER_APPLY) {
1566                Command cmd = createCommand(sel, getChangedTags());
1567                if (cmd != null) {
1568                    Main.main.undoRedo.add(cmd);
1569                }
1570            } else if (answer == DIALOG_ANSWER_NEW_RELATION) {
1571                final Relation r = new Relation();
1572                final Collection<RelationMember> members = new HashSet<RelationMember>();
1573                for(Tag t : getChangedTags()) {
1574                    r.put(t.getKey(), t.getValue());
1575                }
1576                for(OsmPrimitive osm : Main.main.getCurrentDataSet().getSelected()) {
1577                    RelationMember rm = new RelationMember("", osm);
1578                    r.addMember(rm);
1579                    members.add(rm);
1580                }
1581                SwingUtilities.invokeLater(new Runnable() {
1582                    @Override
1583                    public void run() {
1584                        RelationEditor.getEditor(Main.main.getEditLayer(), r, members).setVisible(true);
1585                    }
1586                });
1587            }
1588            Main.main.getCurrentDataSet().setSelected(Main.main.getCurrentDataSet().getSelected()); // force update
1589    
1590        }
1591    
1592        public int showDialog(Collection<OsmPrimitive> sel, final boolean showNewRelation) {
1593            PresetPanel p = createPanel(sel);
1594            if (p == null)
1595                return DIALOG_ANSWER_CANCEL;
1596    
1597            int answer = 1;
1598            if (p.getComponentCount() != 0 && (sel.size() == 0 || p.hasElements)) {
1599                String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
1600                if(sel.size() == 0) {
1601                    if(originalSelectionEmpty) {
1602                        title = tr("Nothing selected!");
1603                    } else {
1604                        title = tr("Selection unsuitable!");
1605                    }
1606                }
1607    
1608                class PresetDialog extends ExtendedDialog {
1609                    public PresetDialog(Component content, String title, boolean disableApply) {
1610                        super(Main.parent,
1611                                title,
1612                                showNewRelation?
1613                                        new String[] { tr("Apply Preset"), tr("New relation"), tr("Cancel") }:
1614                                            new String[] { tr("Apply Preset"), tr("Cancel") },
1615                                            true);
1616                        contentInsets = new Insets(10,5,0,5);
1617                        if (showNewRelation) {
1618                            setButtonIcons(new String[] {"ok.png", "dialogs/addrelation.png", "cancel.png" });
1619                        } else {
1620                            setButtonIcons(new String[] {"ok.png", "cancel.png" });
1621                        }
1622                        setContent(content);
1623                        setDefaultButton(1);
1624                        setupDialog();
1625                        buttons.get(0).setEnabled(!disableApply);
1626                        buttons.get(0).setToolTipText(title);
1627                        // Prevent dialogs of being too narrow (fix #6261)
1628                        Dimension d = getSize();
1629                        if (d.width < 350) {
1630                            d.width = 350;
1631                            setSize(d);
1632                        }
1633                        showDialog();
1634                    }
1635                }
1636    
1637                answer = new PresetDialog(p, title, (sel.size() == 0)).getValue();
1638            }
1639            if (!showNewRelation && answer == 2)
1640                return DIALOG_ANSWER_CANCEL;
1641            else
1642                return answer;
1643        }
1644    
1645        /**
1646         * True whenever the original selection given into createSelection was empty
1647         */
1648        private boolean originalSelectionEmpty = false;
1649    
1650        /**
1651         * Removes all unsuitable OsmPrimitives from the given list
1652         * @param participants List of possible OsmPrimitives to tag
1653         * @return Cleaned list with suitable OsmPrimitives only
1654         */
1655        public Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) {
1656            originalSelectionEmpty = participants.size() == 0;
1657            Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>();
1658            for (OsmPrimitive osm : participants)
1659            {
1660                if (types != null)
1661                {
1662                    if(osm instanceof Relation)
1663                    {
1664                        if(!types.contains(PresetType.RELATION) &&
1665                                !(types.contains(PresetType.CLOSEDWAY) && ((Relation)osm).isMultipolygon())) {
1666                            continue;
1667                        }
1668                    }
1669                    else if(osm instanceof Node)
1670                    {
1671                        if(!types.contains(PresetType.NODE)) {
1672                            continue;
1673                        }
1674                    }
1675                    else if(osm instanceof Way)
1676                    {
1677                        if(!types.contains(PresetType.WAY) &&
1678                                !(types.contains(PresetType.CLOSEDWAY) && ((Way)osm).isClosed())) {
1679                            continue;
1680                        }
1681                    }
1682                }
1683                sel.add(osm);
1684            }
1685            return sel;
1686        }
1687    
1688        public List<Tag> getChangedTags() {
1689            List<Tag> result = new ArrayList<Tag>();
1690            for (Item i: data) {
1691                i.addCommands(result);
1692            }
1693            return result;
1694        }
1695    
1696        private static String fixPresetString(String s) {
1697            return s == null ? s : s.replaceAll("'","''");
1698        }
1699    
1700        public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) {
1701            List<Command> cmds = new ArrayList<Command>();
1702            for (Tag tag: changedTags) {
1703                cmds.add(new ChangePropertyCommand(sel, tag.getKey(), tag.getValue()));
1704            }
1705    
1706            if (cmds.size() == 0)
1707                return null;
1708            else if (cmds.size() == 1)
1709                return cmds.get(0);
1710            else
1711                return new SequenceCommand(tr("Change Properties"), cmds);
1712        }
1713    
1714        private boolean supportsRelation() {
1715            return types == null || types.contains(PresetType.RELATION);
1716        }
1717    
1718        protected void updateEnabledState() {
1719            setEnabled(Main.main != null && Main.main.getCurrentDataSet() != null);
1720        }
1721    
1722        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
1723            updateEnabledState();
1724        }
1725    
1726        public void layerAdded(Layer newLayer) {
1727            updateEnabledState();
1728        }
1729    
1730        public void layerRemoved(Layer oldLayer) {
1731            updateEnabledState();
1732        }
1733    
1734        @Override
1735        public String toString() {
1736            return (types == null?"":types) + " " + name;
1737        }
1738    
1739        public boolean typeMatches(Collection<PresetType> t) {
1740            return t == null || types == null || types.containsAll(t);
1741        }
1742    
1743        public boolean matches(Collection<PresetType> t, Map<String, String> tags, boolean onlyShowable) {
1744            if (onlyShowable && !isShowable())
1745                return false;
1746            else if (!typeMatches(t))
1747                return false;
1748            boolean atLeastOnePositiveMatch = false;
1749            for (Item item : data) {
1750                Boolean m = item.matches(tags);
1751                if (m != null && !m)
1752                    return false;
1753                else if (m != null) {
1754                    atLeastOnePositiveMatch = true;
1755                }
1756            }
1757            return atLeastOnePositiveMatch;
1758        }
1759    }