001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trc;
006
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.Font;
010import java.awt.GridBagLayout;
011import java.awt.GridLayout;
012import java.awt.event.ActionEvent;
013import java.awt.event.ActionListener;
014import java.awt.event.MouseAdapter;
015import java.awt.event.MouseEvent;
016import java.io.File;
017import java.lang.reflect.Method;
018import java.lang.reflect.Modifier;
019import java.text.NumberFormat;
020import java.text.ParseException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.EnumSet;
026import java.util.HashMap;
027import java.util.LinkedHashMap;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.TreeSet;
032
033import javax.swing.ButtonGroup;
034import javax.swing.Icon;
035import javax.swing.ImageIcon;
036import javax.swing.JButton;
037import javax.swing.JComponent;
038import javax.swing.JLabel;
039import javax.swing.JList;
040import javax.swing.JPanel;
041import javax.swing.JScrollPane;
042import javax.swing.JSeparator;
043import javax.swing.JToggleButton;
044import javax.swing.ListCellRenderer;
045import javax.swing.ListModel;
046
047import org.openstreetmap.josm.Main;
048import org.openstreetmap.josm.actions.search.SearchCompiler;
049import org.openstreetmap.josm.data.osm.OsmPrimitive;
050import org.openstreetmap.josm.data.osm.OsmUtils;
051import org.openstreetmap.josm.data.osm.Tag;
052import org.openstreetmap.josm.data.preferences.BooleanProperty;
053import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
054import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionItemPriority;
055import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
056import org.openstreetmap.josm.gui.widgets.JosmComboBox;
057import org.openstreetmap.josm.gui.widgets.JosmTextField;
058import org.openstreetmap.josm.gui.widgets.QuadStateCheckBox;
059import org.openstreetmap.josm.gui.widgets.UrlLabel;
060import org.openstreetmap.josm.tools.GBC;
061import org.openstreetmap.josm.tools.ImageProvider;
062import org.openstreetmap.josm.tools.Predicate;
063import org.openstreetmap.josm.tools.Utils;
064import org.xml.sax.SAXException;
065
066/**
067 * Class that contains all subtypes of TaggingPresetItem, static supplementary data, types and methods
068 * @since 6068
069 */
070public final class TaggingPresetItems {
071    private TaggingPresetItems() {
072    }
073
074    private static int auto_increment_selected = 0;
075    /** Translatation of "<different>". Use in combo boxes to display en entry matching several different values. */
076    public static final String DIFFERENT = tr("<different>");
077
078    private static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
079
080    // 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)
081    private static final Map<String,EnumSet<TaggingPresetType>> TYPE_CACHE = new LinkedHashMap<>(16, 1.1f, true);
082
083    /**
084     * Last value of each key used in presets, used for prefilling corresponding fields
085     */
086    private static final Map<String,String> LAST_VALUES = new HashMap<>();
087
088    public static class PresetListEntry {
089        public String value;
090        /** The context used for translating {@link #value} */
091        public String value_context;
092        public String display_value;
093        public String short_description;
094        /** The location of icon file to display */
095        public String icon;
096        /** The size of displayed icon. If not set, default is size from icon file */
097        public String icon_size;
098        /** The localized version of {@link #display_value}. */
099        public String locale_display_value;
100        /** The localized version of {@link #short_description}. */
101        public String locale_short_description;
102        private final File zipIcons = TaggingPresetReader.getZipIcons();
103
104        // Cached size (currently only for Combo) to speed up preset dialog initialization
105        private int prefferedWidth = -1;
106        private int prefferedHeight = -1;
107
108        public String getListDisplay() {
109            if (value.equals(DIFFERENT))
110                return "<b>"+DIFFERENT.replaceAll("<", "&lt;").replaceAll(">", "&gt;")+"</b>";
111
112            if (value.isEmpty())
113                return "&nbsp;";
114
115            final StringBuilder res = new StringBuilder("<b>");
116            res.append(getDisplayValue(true));
117            res.append("</b>");
118            if (getShortDescription(true) != null) {
119                // wrap in table to restrict the text width
120                res.append("<div style=\"width:300px; padding:0 0 5px 5px\">");
121                res.append(getShortDescription(true));
122                res.append("</div>");
123            }
124            return res.toString();
125        }
126
127        /**
128         * Returns the entry icon, if any.
129         * @return the entry icon, or {@code null}
130         */
131        public ImageIcon getIcon() {
132            return icon == null ? null : loadImageIcon(icon, zipIcons, parseInteger(icon_size));
133        }
134
135        /**
136         * Construxts a new {@code PresetListEntry}, uninitialized.
137         */
138        public PresetListEntry() {
139        }
140
141        public PresetListEntry(String value) {
142            this.value = value;
143        }
144
145        public String getDisplayValue(boolean translated) {
146            return translated
147                    ? Utils.firstNonNull(locale_display_value, tr(display_value), trc(value_context, value))
148                            : Utils.firstNonNull(display_value, value);
149        }
150
151        public String getShortDescription(boolean translated) {
152            return translated
153                    ? Utils.firstNonNull(locale_short_description, tr(short_description))
154                            : short_description;
155        }
156
157        // toString is mainly used to initialize the Editor
158        @Override
159        public String toString() {
160            if (value.equals(DIFFERENT))
161                return DIFFERENT;
162            return getDisplayValue(true).replaceAll("<.*>", ""); // remove additional markup, e.g. <br>
163        }
164    }
165
166    public static class Role {
167        public EnumSet<TaggingPresetType> types;
168        public String key;
169        /** The text to display */
170        public String text;
171        /** The context used for translating {@link #text} */
172        public String text_context;
173        /** The localized version of {@link #text}. */
174        public String locale_text;
175        public SearchCompiler.Match memberExpression;
176
177        public boolean required = false;
178        private long count = 0;
179
180        public void setType(String types) throws SAXException {
181            this.types = getType(types);
182        }
183
184        public void setRequisite(String str) throws SAXException {
185            if("required".equals(str)) {
186                required = true;
187            } else if(!"optional".equals(str))
188                throw new SAXException(tr("Unknown requisite: {0}", str));
189        }
190
191        public void setMember_expression(String member_expression) throws SAXException {
192            try {
193                this.memberExpression = SearchCompiler.compile(member_expression, true, true);
194            } catch (SearchCompiler.ParseError ex) {
195                throw new SAXException(tr("Illegal member expression: {0}", ex.getMessage()), ex);
196            }
197        }
198
199        public void setCount(String count) {
200            this.count = Long.parseLong(count);
201        }
202
203        /**
204         * Return either argument, the highest possible value or the lowest allowed value
205         */
206        public long getValidCount(long c) {
207            if (count > 0 && !required)
208                return c != 0 ? count : 0;
209            else if (count > 0)
210                return count;
211            else if (!required)
212                return c != 0 ? c : 0;
213            else
214                return c != 0 ? c : 1;
215        }
216
217        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
218            String cstring;
219            if (count > 0 && !required) {
220                cstring = "0,"+count;
221            } else if(count > 0) {
222                cstring = String.valueOf(count);
223            } else if(!required) {
224                cstring = "0-...";
225            } else {
226                cstring = "1-...";
227            }
228            if (locale_text == null) {
229                locale_text = getLocaleText(text, text_context, null);
230            }
231            p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
232            p.add(new JLabel(key), GBC.std().insets(0,0,10,0));
233            p.add(new JLabel(cstring), types == null ? GBC.eol() : GBC.std().insets(0,0,10,0));
234            if (types != null) {
235                JPanel pp = new JPanel();
236                for(TaggingPresetType t : types) {
237                    pp.add(new JLabel(ImageProvider.get(t.getIconName())));
238                }
239                p.add(pp, GBC.eol());
240            }
241            return true;
242        }
243    }
244
245    /**
246     * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed.
247     */
248    public static enum MatchType {
249
250        /** Neutral, i.e., do not consider this item for matching. */
251        NONE("none"),
252        /** Positive if key matches, neutral otherwise. */
253        KEY("key"),
254        /** Positive if key matches, negative otherwise. */
255        KEY_REQUIRED("key!"),
256        /** Positive if key and value matches, neutral otherwise. */
257        KEY_VALUE("keyvalue"),
258        /** Positive if key and value matches, negative otherwise. */
259        KEY_VALUE_REQUIRED("keyvalue!");
260
261        private final String value;
262
263        private MatchType(String value) {
264            this.value = value;
265        }
266
267        /**
268         * Replies the associated textual value.
269         * @return the associated textual value
270         */
271        public String getValue() {
272            return value;
273        }
274
275        /**
276         * Determines the {@code MatchType} for the given textual value.
277         * @param type the textual value
278         * @return the {@code MatchType} for the given textual value
279         */
280        public static MatchType ofString(String type) {
281            for (MatchType i : EnumSet.allOf(MatchType.class)) {
282                if (i.getValue().equals(type))
283                    return i;
284            }
285            throw new IllegalArgumentException(type + " is not allowed");
286        }
287    }
288
289    public static class Usage {
290        TreeSet<String> values;
291        boolean hadKeys = false;
292        boolean hadEmpty = false;
293
294        public boolean hasUniqueValue() {
295            return values.size() == 1 && !hadEmpty;
296        }
297
298        public boolean unused() {
299            return values.isEmpty();
300        }
301
302        public String getFirst() {
303            return values.first();
304        }
305
306        public boolean hadKeys() {
307            return hadKeys;
308        }
309    }
310
311    /**
312     * A tagging preset item displaying a localizable text.
313     * @since 6190
314     */
315    public abstract static class TaggingPresetTextItem extends TaggingPresetItem {
316
317        /** The text to display */
318        public String text;
319
320        /** The context used for translating {@link #text} */
321        public String text_context;
322
323        /** The localized version of {@link #text} */
324        public String locale_text;
325
326        protected final void initializeLocaleText(String defaultText) {
327            if (locale_text == null) {
328                locale_text = getLocaleText(text, text_context, defaultText);
329            }
330        }
331
332        @Override
333        void addCommands(List<Tag> changedTags) {
334        }
335
336        protected String fieldsToString() {
337            return (text != null ? "text=" + text + ", " : "")
338                    + (text_context != null ? "text_context=" + text_context + ", " : "")
339                    + (locale_text != null ? "locale_text=" + locale_text : "");
340        }
341
342        @Override
343        public String toString() {
344            return getClass().getSimpleName() + " [" + fieldsToString() + "]";
345        }
346    }
347
348    /**
349     * Label type.
350     */
351    public static class Label extends TaggingPresetTextItem {
352
353        /** The location of icon file to display (optional) */
354        public String icon;
355        /** The size of displayed icon. If not set, default is 16px */
356        public String icon_size;
357
358        @Override
359        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
360            initializeLocaleText(null);
361            addLabel(p, getIcon(), locale_text);
362            return true;
363        }
364
365        /**
366         * Adds a new {@code JLabel} to the given panel.
367         * @param p The panel
368         * @param icon the icon (optional, can be null)
369         * @param label The text label
370         */
371        public static void addLabel(JPanel p, Icon icon, String label) {
372            p.add(new JLabel(label, icon, JLabel.LEADING), GBC.eol().fill(GBC.HORIZONTAL));
373        }
374
375        /**
376         * Returns the label icon, if any.
377         * @return the label icon, or {@code null}
378         */
379        public ImageIcon getIcon() {
380            Integer size = parseInteger(icon_size);
381            return icon == null ? null : loadImageIcon(icon, TaggingPresetReader.getZipIcons(), size != null ? size : 16);
382        }
383    }
384
385    /**
386     * Hyperlink type.
387     */
388    public static class Link extends TaggingPresetTextItem {
389
390        /** The link to display. */
391        public String href;
392
393        /** The localized version of {@link #href}. */
394        public String locale_href;
395
396        @Override
397        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
398            initializeLocaleText(tr("More information about this feature"));
399            String url = locale_href;
400            if (url == null) {
401                url = href;
402            }
403            if (url != null) {
404                p.add(new UrlLabel(url, locale_text, 2), GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL));
405            }
406            return false;
407        }
408
409        @Override
410        protected String fieldsToString() {
411            return super.fieldsToString()
412                    + (href != null ? "href=" + href + ", " : "")
413                    + (locale_href != null ? "locale_href=" + locale_href + ", " : "");
414        }
415    }
416
417    public static class PresetLink extends TaggingPresetItem {
418
419        public String preset_name = "";
420
421        @Override
422        boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
423            final String presetName = preset_name;
424            final TaggingPreset t = Utils.filter(TaggingPresets.getTaggingPresets(), new Predicate<TaggingPreset>() {
425                @Override
426                public boolean evaluate(TaggingPreset object) {
427                    return presetName.equals(object.name);
428                }
429            }).iterator().next();
430            if (t == null) return false;
431            JLabel lbl = new PresetLabel(t);
432            lbl.addMouseListener(new MouseAdapter() {
433                @Override
434                public void mouseClicked(MouseEvent arg0) {
435                    t.actionPerformed(null);
436                }
437            });
438            p.add(lbl, GBC.eol().fill(GBC.HORIZONTAL));
439            return false;
440        }
441
442        @Override
443        void addCommands(List<Tag> changedTags) {
444        }
445    }
446
447    public static class Roles extends TaggingPresetItem {
448
449        public final List<Role> roles = new LinkedList<>();
450
451        @Override
452        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
453            p.add(new JLabel(" "), GBC.eol()); // space
454            if (!roles.isEmpty()) {
455                JPanel proles = new JPanel(new GridBagLayout());
456                proles.add(new JLabel(tr("Available roles")), GBC.std().insets(0, 0, 10, 0));
457                proles.add(new JLabel(tr("role")), GBC.std().insets(0, 0, 10, 0));
458                proles.add(new JLabel(tr("count")), GBC.std().insets(0, 0, 10, 0));
459                proles.add(new JLabel(tr("elements")), GBC.eol());
460                for (Role i : roles) {
461                    i.addToPanel(proles, sel);
462                }
463                p.add(proles, GBC.eol());
464            }
465            return false;
466        }
467
468        @Override
469        public void addCommands(List<Tag> changedTags) {
470        }
471    }
472
473    public static class Optional extends TaggingPresetTextItem {
474
475        // TODO: Draw a box around optional stuff
476        @Override
477        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
478            initializeLocaleText(tr("Optional Attributes:"));
479            p.add(new JLabel(" "), GBC.eol()); // space
480            p.add(new JLabel(locale_text), GBC.eol());
481            p.add(new JLabel(" "), GBC.eol()); // space
482            return false;
483        }
484    }
485
486    /**
487     * Horizontal separator type.
488     */
489    public static class Space extends TaggingPresetItem {
490
491        @Override
492        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
493            p.add(new JLabel(" "), GBC.eol()); // space
494            return false;
495        }
496
497        @Override
498        public void addCommands(List<Tag> changedTags) {
499        }
500
501        @Override
502        public String toString() {
503            return "Space";
504        }
505    }
506
507    /**
508     * Class used to represent a {@link JSeparator} inside tagging preset window.
509     * @since 6198
510     */
511    public static class ItemSeparator extends TaggingPresetItem {
512
513        @Override
514        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
515            p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
516            return false;
517        }
518
519        @Override
520        public void addCommands(List<Tag> changedTags) {
521        }
522
523        @Override
524        public String toString() {
525            return "ItemSeparator";
526        }
527    }
528
529    /**
530     * Preset item associated to an OSM key.
531     */
532    public abstract static class KeyedItem extends TaggingPresetItem {
533
534        public String key;
535        /** The text to display */
536        public String text;
537        /** The context used for translating {@link #text} */
538        public String text_context;
539        public String match = getDefaultMatch().getValue();
540
541        public abstract MatchType getDefaultMatch();
542        public abstract Collection<String> getValues();
543
544        @Override
545        Boolean matches(Map<String, String> tags) {
546            switch (MatchType.ofString(match)) {
547            case NONE:
548                return null;
549            case KEY:
550                return tags.containsKey(key) ? true : null;
551            case KEY_REQUIRED:
552                return tags.containsKey(key);
553            case KEY_VALUE:
554                return tags.containsKey(key) && getValues().contains(tags.get(key)) ? true : null;
555            case KEY_VALUE_REQUIRED:
556                return tags.containsKey(key) && getValues().contains(tags.get(key));
557            default:
558                throw new IllegalStateException();
559            }
560        }
561
562        @Override
563        public String toString() {
564            return "KeyedItem [key=" + key + ", text=" + text
565                    + ", text_context=" + text_context + ", match=" + match
566                    + "]";
567        }
568    }
569
570    /**
571     * Invisible type allowing to hardcode an OSM key/value from the preset definition.
572     */
573    public static class Key extends KeyedItem {
574
575        /** The hardcoded value for key */
576        public String value;
577
578        @Override
579        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
580            return false;
581        }
582
583        @Override
584        public void addCommands(List<Tag> changedTags) {
585            changedTags.add(new Tag(key, value));
586        }
587
588        @Override
589        public MatchType getDefaultMatch() {
590            return MatchType.KEY_VALUE_REQUIRED;
591        }
592
593        @Override
594        public Collection<String> getValues() {
595            return Collections.singleton(value);
596        }
597
598        @Override
599        public String toString() {
600            return "Key [key=" + key + ", value=" + value + ", text=" + text
601                    + ", text_context=" + text_context + ", match=" + match
602                    + "]";
603        }
604    }
605
606    /**
607     * Text field type.
608     */
609    public static class Text extends KeyedItem {
610
611        /** The localized version of {@link #text}. */
612        public String locale_text;
613        public String default_;
614        public String originalValue;
615        public String use_last_as_default = "false";
616        public String auto_increment;
617        public String length;
618        public String alternative_autocomplete_keys;
619
620        private JComponent value;
621
622        @Override
623        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
624
625            // find out if our key is already used in the selection.
626            Usage usage = determineTextUsage(sel, key);
627            AutoCompletingTextField textField = new AutoCompletingTextField();
628            if (alternative_autocomplete_keys != null) {
629                initAutoCompletionField(textField, (key + "," + alternative_autocomplete_keys).split(","));
630            } else {
631                initAutoCompletionField(textField, key);
632            }
633            if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) {
634                textField.setHint(key);
635            }
636            if (length != null && !length.isEmpty()) {
637                textField.setMaxChars(Integer.valueOf(length));
638            }
639            if (usage.unused()){
640                if (auto_increment_selected != 0  && auto_increment != null) {
641                    try {
642                        textField.setText(Integer.toString(Integer.parseInt(LAST_VALUES.get(key)) + auto_increment_selected));
643                    } catch (NumberFormatException ex) {
644                        // Ignore - cannot auto-increment if last was non-numeric
645                    }
646                }
647                else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
648                    // selected osm primitives are untagged or filling default values feature is enabled
649                    if (!"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) {
650                        textField.setText(LAST_VALUES.get(key));
651                    } else {
652                        textField.setText(default_);
653                    }
654                } else {
655                    // selected osm primitives are tagged and filling default values feature is disabled
656                    textField.setText("");
657                }
658                value = textField;
659                originalValue = null;
660            } else if (usage.hasUniqueValue()) {
661                // all objects use the same value
662                textField.setText(usage.getFirst());
663                value = textField;
664                originalValue = usage.getFirst();
665            } else {
666                // the objects have different values
667                JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[0]));
668                comboBox.setEditable(true);
669                comboBox.setEditor(textField);
670                comboBox.getEditor().setItem(DIFFERENT);
671                value=comboBox;
672                originalValue = DIFFERENT;
673            }
674            if (locale_text == null) {
675                locale_text = getLocaleText(text, text_context, null);
676            }
677
678            // if there's an auto_increment setting, then wrap the text field
679            // into a panel, appending a number of buttons.
680            // auto_increment has a format like -2,-1,1,2
681            // the text box being the first component in the panel is relied
682            // on in a rather ugly fashion further down.
683            if (auto_increment != null) {
684                ButtonGroup bg = new ButtonGroup();
685                JPanel pnl = new JPanel(new GridBagLayout());
686                pnl.add(value, GBC.std().fill(GBC.HORIZONTAL));
687
688                // first, one button for each auto_increment value
689                for (final String ai : auto_increment.split(",")) {
690                    JToggleButton aibutton = new JToggleButton(ai);
691                    aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai));
692                    aibutton.setMargin(new java.awt.Insets(0,0,0,0));
693                    aibutton.setFocusable(false);
694                    bg.add(aibutton);
695                    try {
696                        // TODO there must be a better way to parse a number like "+3" than this.
697                        final int buttonvalue = (NumberFormat.getIntegerInstance().parse(ai.replace("+", ""))).intValue();
698                        if (auto_increment_selected == buttonvalue) aibutton.setSelected(true);
699                        aibutton.addActionListener(new ActionListener() {
700                            @Override
701                            public void actionPerformed(ActionEvent e) {
702                                auto_increment_selected = buttonvalue;
703                            }
704                        });
705                        pnl.add(aibutton, GBC.std());
706                    } catch (ParseException x) {
707                        Main.error("Cannot parse auto-increment value of '" + ai + "' into an integer");
708                    }
709                }
710
711                // an invisible toggle button for "release" of the button group
712                final JToggleButton clearbutton = new JToggleButton("X");
713                clearbutton.setVisible(false);
714                clearbutton.setFocusable(false);
715                bg.add(clearbutton);
716                // and its visible counterpart. - this mechanism allows us to
717                // have *no* button selected after the X is clicked, instead
718                // of the X remaining selected
719                JButton releasebutton = new JButton("X");
720                releasebutton.setToolTipText(tr("Cancel auto-increment for this field"));
721                releasebutton.setMargin(new java.awt.Insets(0,0,0,0));
722                releasebutton.setFocusable(false);
723                releasebutton.addActionListener(new ActionListener() {
724                    @Override
725                    public void actionPerformed(ActionEvent e) {
726                        auto_increment_selected = 0;
727                        clearbutton.setSelected(true);
728                    }
729                });
730                pnl.add(releasebutton, GBC.eol());
731                value = pnl;
732            }
733            p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
734            p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
735            return true;
736        }
737
738        private static String getValue(Component comp) {
739            if (comp instanceof JosmComboBox) {
740                return ((JosmComboBox<?>) comp).getEditor().getItem().toString();
741            } else if (comp instanceof JosmTextField) {
742                return ((JosmTextField) comp).getText();
743            } else if (comp instanceof JPanel) {
744                return getValue(((JPanel)comp).getComponent(0));
745            } else {
746                return null;
747            }
748        }
749
750        @Override
751        public void addCommands(List<Tag> changedTags) {
752
753            // return if unchanged
754            String v = getValue(value);
755            if (v == null) {
756                Main.error("No 'last value' support for component " + value);
757                return;
758            }
759
760            v = Tag.removeWhiteSpaces(v);
761
762            if (!"false".equals(use_last_as_default) || auto_increment != null) {
763                LAST_VALUES.put(key, v);
764            }
765            if (v.equals(originalValue) || (originalValue == null && v.length() == 0))
766                return;
767
768            changedTags.add(new Tag(key, v));
769        }
770
771        @Override
772        boolean requestFocusInWindow() {
773            return value.requestFocusInWindow();
774        }
775
776        @Override
777        public MatchType getDefaultMatch() {
778            return MatchType.NONE;
779        }
780
781        @Override
782        public Collection<String> getValues() {
783            if (default_ == null || default_.isEmpty())
784                return Collections.emptyList();
785            return Collections.singleton(default_);
786        }
787    }
788
789    /**
790     * A group of {@link Check}s.
791     * @since 6114
792     */
793    public static class CheckGroup extends TaggingPresetItem {
794
795        /**
796         * Number of columns (positive integer)
797         */
798        public String columns;
799
800        /**
801         * List of checkboxes
802         */
803        public final List<Check> checks = new LinkedList<>();
804
805        @Override
806        boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
807            Integer cols = Integer.valueOf(columns);
808            int rows = (int) Math.ceil(checks.size()/cols.doubleValue());
809            JPanel panel = new JPanel(new GridLayout(rows, cols));
810
811            for (Check check : checks) {
812                check.addToPanel(panel, sel, presetInitiallyMatches);
813            }
814
815            p.add(panel, GBC.eol());
816            return false;
817        }
818
819        @Override
820        void addCommands(List<Tag> changedTags) {
821            for (Check check : checks) {
822                check.addCommands(changedTags);
823            }
824        }
825
826        @Override
827        public String toString() {
828            return "CheckGroup [columns=" + columns + "]";
829        }
830    }
831
832    /**
833     * Checkbox type.
834     */
835    public static class Check extends KeyedItem {
836
837        /** The localized version of {@link #text}. */
838        public String locale_text;
839        /** the value to set when checked (default is "yes") */
840        public String value_on = OsmUtils.trueval;
841        /** the value to set when unchecked (default is "no") */
842        public String value_off = OsmUtils.falseval;
843        /** whether the off value is disabled in the dialog, i.e., only unset or yes are provided */
844        public boolean disable_off = false;
845        /** ticked on/off (default is "off") */
846        public boolean default_ = false; // only used for tagless objects
847
848        private QuadStateCheckBox check;
849        private QuadStateCheckBox.State initialState;
850        private boolean def;
851
852        @Override
853        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
854
855            // find out if our key is already used in the selection.
856            final Usage usage = determineBooleanUsage(sel, key);
857            final String oneValue = usage.values.isEmpty() ? null : usage.values.last();
858            def = default_;
859
860            if (locale_text == null) {
861                locale_text = getLocaleText(text, text_context, null);
862            }
863
864            if (usage.values.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) {
865                if (def && !PROP_FILL_DEFAULT.get()) {
866                    // default is set and filling default values feature is disabled - check if all primitives are untagged
867                    for (OsmPrimitive s : sel)
868                        if (s.hasKeys()) {
869                            def = false;
870                        }
871                }
872
873                // all selected objects share the same value which is either true or false or unset,
874                // we can display a standard check box.
875                initialState = value_on.equals(oneValue)
876                        ? QuadStateCheckBox.State.SELECTED
877                        : value_off.equals(oneValue)
878                        ? QuadStateCheckBox.State.NOT_SELECTED
879                        : def
880                        ? QuadStateCheckBox.State.SELECTED
881                        : QuadStateCheckBox.State.UNSET;
882            } else {
883                def = false;
884                // the objects have different values, or one or more objects have something
885                // else than true/false. we display a quad-state check box
886                // in "partial" state.
887                initialState = QuadStateCheckBox.State.PARTIAL;
888            }
889
890            final List<QuadStateCheckBox.State> allowedStates = new ArrayList<>(4);
891            if (QuadStateCheckBox.State.PARTIAL.equals(initialState))
892                allowedStates.add(QuadStateCheckBox.State.PARTIAL);
893            allowedStates.add(QuadStateCheckBox.State.SELECTED);
894            if (!disable_off || value_off.equals(oneValue))
895                allowedStates.add(QuadStateCheckBox.State.NOT_SELECTED);
896            allowedStates.add(QuadStateCheckBox.State.UNSET);
897            check = new QuadStateCheckBox(locale_text, initialState,
898                    allowedStates.toArray(new QuadStateCheckBox.State[allowedStates.size()]));
899
900            p.add(check, GBC.eol().fill(GBC.HORIZONTAL));
901            return true;
902        }
903
904        @Override
905        public void addCommands(List<Tag> changedTags) {
906            // if the user hasn't changed anything, don't create a command.
907            if (check.getState() == initialState && !def) return;
908
909            // otherwise change things according to the selected value.
910            changedTags.add(new Tag(key,
911                    check.getState() == QuadStateCheckBox.State.SELECTED ? value_on :
912                        check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? value_off :
913                            null));
914        }
915
916        @Override
917        boolean requestFocusInWindow() {return check.requestFocusInWindow();}
918
919        @Override
920        public MatchType getDefaultMatch() {
921            return MatchType.NONE;
922        }
923
924        @Override
925        public Collection<String> getValues() {
926            return disable_off ? Arrays.asList(value_on) : Arrays.asList(value_on, value_off);
927        }
928
929        @Override
930        public String toString() {
931            return "Check ["
932                    + (locale_text != null ? "locale_text=" + locale_text + ", " : "")
933                    + (value_on != null ? "value_on=" + value_on + ", " : "")
934                    + (value_off != null ? "value_off=" + value_off + ", " : "")
935                    + "default_=" + default_ + ", "
936                    + (check != null ? "check=" + check + ", " : "")
937                    + (initialState != null ? "initialState=" + initialState
938                            + ", " : "") + "def=" + def + "]";
939        }
940    }
941
942    /**
943     * Abstract superclass for combo box and multi-select list types.
944     */
945    public abstract static class ComboMultiSelect extends KeyedItem {
946
947        /** The localized version of {@link #text}. */
948        public String locale_text;
949        public String values;
950        public String values_from;
951        /** The context used for translating {@link #values} */
952        public String values_context;
953        public String display_values;
954        /** The localized version of {@link #display_values}. */
955        public String locale_display_values;
956        public String short_descriptions;
957        /** The localized version of {@link #short_descriptions}. */
958        public String locale_short_descriptions;
959        public String default_;
960        public String delimiter = ";";
961        public String use_last_as_default = "false";
962        /** whether to use values for search via {@link TaggingPresetSelector} */
963        public String values_searchable = "false";
964
965        protected JComponent component;
966        protected final Map<String, PresetListEntry> lhm = new LinkedHashMap<>();
967        private boolean initialized = false;
968        protected Usage usage;
969        protected Object originalValue;
970
971        protected abstract Object getSelectedItem();
972        protected abstract void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches);
973
974        protected char getDelChar() {
975            return delimiter.isEmpty() ? ';' : delimiter.charAt(0);
976        }
977
978        @Override
979        public Collection<String> getValues() {
980            initListEntries();
981            return lhm.keySet();
982        }
983
984        public Collection<String> getDisplayValues() {
985            initListEntries();
986            return Utils.transform(lhm.values(), new Utils.Function<PresetListEntry, String>() {
987                @Override
988                public String apply(PresetListEntry x) {
989                    return x.getDisplayValue(true);
990                }
991            });
992        }
993
994        @Override
995        public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
996
997            initListEntries();
998
999            // find out if our key is already used in the selection.
1000            usage = determineTextUsage(sel, key);
1001            if (!usage.hasUniqueValue() && !usage.unused()) {
1002                lhm.put(DIFFERENT, new PresetListEntry(DIFFERENT));
1003            }
1004
1005            p.add(new JLabel(tr("{0}:", locale_text)), GBC.std().insets(0, 0, 10, 0));
1006            addToPanelAnchor(p, default_, presetInitiallyMatches);
1007
1008            return true;
1009
1010        }
1011
1012        private void initListEntries() {
1013            if (initialized) {
1014                lhm.remove(DIFFERENT); // possibly added in #addToPanel
1015                return;
1016            } else if (lhm.isEmpty()) {
1017                initListEntriesFromAttributes();
1018            } else {
1019                if (values != null) {
1020                    Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
1021                            + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
1022                            key, text, "values", "list_entry"));
1023                }
1024                if (display_values != null || locale_display_values != null) {
1025                    Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
1026                            + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
1027                            key, text, "display_values", "list_entry"));
1028                }
1029                if (short_descriptions != null || locale_short_descriptions != null) {
1030                    Main.warn(tr("Warning in tagging preset \"{0}-{1}\": "
1031                            + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.",
1032                            key, text, "short_descriptions", "list_entry"));
1033                }
1034                for (PresetListEntry e : lhm.values()) {
1035                    if (e.value_context == null) {
1036                        e.value_context = values_context;
1037                    }
1038                }
1039            }
1040            if (locale_text == null) {
1041                locale_text = getLocaleText(text, text_context, null);
1042            }
1043            initialized = true;
1044        }
1045
1046        private String[] initListEntriesFromAttributes() {
1047            char delChar = getDelChar();
1048
1049            String[] value_array = null;
1050
1051            if (values_from != null) {
1052                String[] class_method = values_from.split("#");
1053                if (class_method != null && class_method.length == 2) {
1054                    try {
1055                        Method method = Class.forName(class_method[0]).getMethod(class_method[1]);
1056                        // Check method is public static String[] methodName()
1057                        int mod = method.getModifiers();
1058                        if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
1059                                && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) {
1060                            value_array = (String[]) method.invoke(null);
1061                        } else {
1062                            Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text,
1063                                    "public static String[] methodName()"));
1064                        }
1065                    } catch (Exception e) {
1066                        Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text,
1067                                e.getClass().getName(), e.getMessage()));
1068                    }
1069                }
1070            }
1071
1072            if (value_array == null) {
1073                value_array = splitEscaped(delChar, values);
1074            }
1075
1076            final String displ = Utils.firstNonNull(locale_display_values, display_values);
1077            String[] display_array = displ == null ? value_array : splitEscaped(delChar, displ);
1078
1079            final String descr = Utils.firstNonNull(locale_short_descriptions, short_descriptions);
1080            String[] short_descriptions_array = descr == null ? null : splitEscaped(delChar, descr);
1081
1082            if (display_array.length != value_array.length) {
1083                Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''display_values'' must be the same as in ''values''", key, text));
1084                display_array = value_array;
1085            }
1086
1087            if (short_descriptions_array != null && short_descriptions_array.length != value_array.length) {
1088                Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''short_descriptions'' must be the same as in ''values''", key, text));
1089                short_descriptions_array = null;
1090            }
1091
1092            for (int i = 0; i < value_array.length; i++) {
1093                final PresetListEntry e = new PresetListEntry(value_array[i]);
1094                e.locale_display_value = locale_display_values != null
1095                        ? display_array[i]
1096                                : trc(values_context, fixPresetString(display_array[i]));
1097                        if (short_descriptions_array != null) {
1098                            e.locale_short_description = locale_short_descriptions != null
1099                                    ? short_descriptions_array[i]
1100                                            : tr(fixPresetString(short_descriptions_array[i]));
1101                        }
1102                        lhm.put(value_array[i], e);
1103                        display_array[i] = e.getDisplayValue(true);
1104            }
1105
1106            return display_array;
1107        }
1108
1109        protected String getDisplayIfNull() {
1110            return null;
1111        }
1112
1113        @Override
1114        public void addCommands(List<Tag> changedTags) {
1115            Object obj = getSelectedItem();
1116            String display = (obj == null) ? null : obj.toString();
1117            String value = null;
1118            if (display == null) {
1119                display = getDisplayIfNull();
1120            }
1121
1122            if (display != null) {
1123                for (String val : lhm.keySet()) {
1124                    String k = lhm.get(val).toString();
1125                    if (k != null && k.equals(display)) {
1126                        value = val;
1127                        break;
1128                    }
1129                }
1130                if (value == null) {
1131                    value = display;
1132                }
1133            } else {
1134                value = "";
1135            }
1136            value = Tag.removeWhiteSpaces(value);
1137
1138            // no change if same as before
1139            if (originalValue == null) {
1140                if (value.length() == 0)
1141                    return;
1142            } else if (value.equals(originalValue.toString()))
1143                return;
1144
1145            if (!"false".equals(use_last_as_default)) {
1146                LAST_VALUES.put(key, value);
1147            }
1148            changedTags.add(new Tag(key, value));
1149        }
1150
1151        public void addListEntry(PresetListEntry e) {
1152            lhm.put(e.value, e);
1153        }
1154
1155        public void addListEntries(Collection<PresetListEntry> e) {
1156            for (PresetListEntry i : e) {
1157                addListEntry(i);
1158            }
1159        }
1160
1161        @Override
1162        boolean requestFocusInWindow() {
1163            return component.requestFocusInWindow();
1164        }
1165
1166        private static final ListCellRenderer<PresetListEntry> RENDERER = new ListCellRenderer<PresetListEntry>() {
1167
1168            JLabel lbl = new JLabel();
1169
1170            @Override
1171            public Component getListCellRendererComponent(
1172                    JList<? extends PresetListEntry> list,
1173                    PresetListEntry item,
1174                    int index,
1175                    boolean isSelected,
1176                    boolean cellHasFocus) {
1177
1178                // Only return cached size, item is not shown
1179                if (!list.isShowing() && item.prefferedWidth != -1 && item.prefferedHeight != -1) {
1180                    if (index == -1) {
1181                        lbl.setPreferredSize(new Dimension(item.prefferedWidth, 10));
1182                    } else {
1183                        lbl.setPreferredSize(new Dimension(item.prefferedWidth, item.prefferedHeight));
1184                    }
1185                    return lbl;
1186                }
1187
1188                lbl.setPreferredSize(null);
1189
1190
1191                if (isSelected) {
1192                    lbl.setBackground(list.getSelectionBackground());
1193                    lbl.setForeground(list.getSelectionForeground());
1194                } else {
1195                    lbl.setBackground(list.getBackground());
1196                    lbl.setForeground(list.getForeground());
1197                }
1198
1199                lbl.setOpaque(true);
1200                lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
1201                lbl.setText("<html>" + item.getListDisplay() + "</html>");
1202                lbl.setIcon(item.getIcon());
1203                lbl.setEnabled(list.isEnabled());
1204
1205                // Cache size
1206                item.prefferedWidth = lbl.getPreferredSize().width;
1207                item.prefferedHeight = lbl.getPreferredSize().height;
1208
1209                // We do not want the editor to have the maximum height of all
1210                // entries. Return a dummy with bogus height.
1211                if (index == -1) {
1212                    lbl.setPreferredSize(new Dimension(lbl.getPreferredSize().width, 10));
1213                }
1214                return lbl;
1215            }
1216        };
1217
1218        protected ListCellRenderer<PresetListEntry> getListCellRenderer() {
1219            return RENDERER;
1220        }
1221
1222        @Override
1223        public MatchType getDefaultMatch() {
1224            return MatchType.NONE;
1225        }
1226    }
1227
1228    /**
1229     * Combobox type.
1230     */
1231    public static class Combo extends ComboMultiSelect {
1232
1233        public boolean editable = true;
1234        protected JosmComboBox<PresetListEntry> combo;
1235        public String length;
1236
1237        /**
1238         * Constructs a new {@code Combo}.
1239         */
1240        public Combo() {
1241            delimiter = ",";
1242        }
1243
1244        @Override
1245        protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) {
1246            if (!usage.unused()) {
1247                for (String s : usage.values) {
1248                    if (!lhm.containsKey(s)) {
1249                        lhm.put(s, new PresetListEntry(s));
1250                    }
1251                }
1252            }
1253            if (def != null && !lhm.containsKey(def)) {
1254                lhm.put(def, new PresetListEntry(def));
1255            }
1256            lhm.put("", new PresetListEntry(""));
1257
1258            combo = new JosmComboBox<>(lhm.values().toArray(new PresetListEntry[0]));
1259            component = combo;
1260            combo.setRenderer(getListCellRenderer());
1261            combo.setEditable(editable);
1262            combo.reinitialize(lhm.values());
1263            AutoCompletingTextField tf = new AutoCompletingTextField();
1264            initAutoCompletionField(tf, key);
1265            if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) {
1266                tf.setHint(key);
1267            }
1268            if (length != null && !length.isEmpty()) {
1269                tf.setMaxChars(Integer.valueOf(length));
1270            }
1271            AutoCompletionList acList = tf.getAutoCompletionList();
1272            if (acList != null) {
1273                acList.add(getDisplayValues(), AutoCompletionItemPriority.IS_IN_STANDARD);
1274            }
1275            combo.setEditor(tf);
1276
1277            if (usage.hasUniqueValue()) {
1278                // all items have the same value (and there were no unset items)
1279                originalValue = lhm.get(usage.getFirst());
1280                combo.setSelectedItem(originalValue);
1281            } else if (def != null && usage.unused()) {
1282                // default is set and all items were unset
1283                if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
1284                    // selected osm primitives are untagged or filling default feature is enabled
1285                    combo.setSelectedItem(lhm.get(def).getDisplayValue(true));
1286                } else {
1287                    // selected osm primitives are tagged and filling default feature is disabled
1288                    combo.setSelectedItem("");
1289                }
1290                originalValue = lhm.get(DIFFERENT);
1291            } else if (usage.unused()) {
1292                // all items were unset (and so is default)
1293                originalValue = lhm.get("");
1294                if ("force".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) {
1295                    combo.setSelectedItem(lhm.get(LAST_VALUES.get(key)));
1296                } else {
1297                    combo.setSelectedItem(originalValue);
1298                }
1299            } else {
1300                originalValue = lhm.get(DIFFERENT);
1301                combo.setSelectedItem(originalValue);
1302            }
1303            p.add(combo, GBC.eol().fill(GBC.HORIZONTAL));
1304
1305        }
1306
1307        @Override
1308        protected Object getSelectedItem() {
1309            return combo.getSelectedItem();
1310
1311        }
1312
1313        @Override
1314        protected String getDisplayIfNull() {
1315            if (combo.isEditable())
1316                return combo.getEditor().getItem().toString();
1317            else
1318                return null;
1319        }
1320    }
1321
1322    /**
1323     * Multi-select list type.
1324     */
1325    public static class MultiSelect extends ComboMultiSelect {
1326
1327        public long rows = -1;
1328        protected ConcatenatingJList list;
1329
1330        @Override
1331        protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) {
1332            list = new ConcatenatingJList(delimiter, lhm.values().toArray(new PresetListEntry[0]));
1333            component = list;
1334            ListCellRenderer<PresetListEntry> renderer = getListCellRenderer();
1335            list.setCellRenderer(renderer);
1336
1337            if (usage.hasUniqueValue() && !usage.unused()) {
1338                originalValue = usage.getFirst();
1339                list.setSelectedItem(originalValue);
1340            } else if (def != null && !usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
1341                originalValue = DIFFERENT;
1342                list.setSelectedItem(def);
1343            } else if (usage.unused()) {
1344                originalValue = null;
1345                list.setSelectedItem(originalValue);
1346            } else {
1347                originalValue = DIFFERENT;
1348                list.setSelectedItem(originalValue);
1349            }
1350
1351            JScrollPane sp = new JScrollPane(list);
1352            // if a number of rows has been specified in the preset,
1353            // modify preferred height of scroll pane to match that row count.
1354            if (rows != -1) {
1355                double height = renderer.getListCellRendererComponent(list,
1356                        new PresetListEntry("x"), 0, false, false).getPreferredSize().getHeight() * rows;
1357                sp.setPreferredSize(new Dimension((int) sp.getPreferredSize().getWidth(), (int) height));
1358            }
1359            p.add(sp, GBC.eol().fill(GBC.HORIZONTAL));
1360        }
1361
1362        @Override
1363        protected Object getSelectedItem() {
1364            return list.getSelectedItem();
1365        }
1366
1367        @Override
1368        public void addCommands(List<Tag> changedTags) {
1369            // Do not create any commands if list has been disabled because of an unknown value (fix #8605)
1370            if (list.isEnabled()) {
1371                super.addCommands(changedTags);
1372            }
1373        }
1374    }
1375
1376    /**
1377    * Class that allows list values to be assigned and retrieved as a comma-delimited
1378    * string (extracted from TaggingPreset)
1379    */
1380    private static class ConcatenatingJList extends JList<PresetListEntry> {
1381        private String delimiter;
1382        public ConcatenatingJList(String del, PresetListEntry[] o) {
1383            super(o);
1384            delimiter = del;
1385        }
1386
1387        public void setSelectedItem(Object o) {
1388            if (o == null) {
1389                clearSelection();
1390            } else {
1391                String s = o.toString();
1392                TreeSet<String> parts = new TreeSet<>(Arrays.asList(s.split(delimiter)));
1393                ListModel<PresetListEntry> lm = getModel();
1394                int[] intParts = new int[lm.getSize()];
1395                int j = 0;
1396                for (int i = 0; i < lm.getSize(); i++) {
1397                    if (parts.contains((lm.getElementAt(i).value))) {
1398                        intParts[j++]=i;
1399                    }
1400                }
1401                setSelectedIndices(Arrays.copyOf(intParts, j));
1402                // check if we have actually managed to represent the full
1403                // value with our presets. if not, cop out; we will not offer
1404                // a selection list that threatens to ruin the value.
1405                setEnabled(Utils.join(delimiter, parts).equals(getSelectedItem()));
1406            }
1407        }
1408
1409        public String getSelectedItem() {
1410            ListModel<PresetListEntry> lm = getModel();
1411            int[] si = getSelectedIndices();
1412            StringBuilder builder = new StringBuilder();
1413            for (int i=0; i<si.length; i++) {
1414                if (i>0) {
1415                    builder.append(delimiter);
1416                }
1417                builder.append(lm.getElementAt(si[i]).value);
1418            }
1419            return builder.toString();
1420        }
1421    }
1422
1423    public static EnumSet<TaggingPresetType> getType(String types) throws SAXException {
1424        if (TYPE_CACHE.containsKey(types))
1425            return TYPE_CACHE.get(types);
1426        EnumSet<TaggingPresetType> result = EnumSet.noneOf(TaggingPresetType.class);
1427        for (String type : Arrays.asList(types.split(","))) {
1428            try {
1429                TaggingPresetType presetType = TaggingPresetType.fromString(type);
1430                result.add(presetType);
1431            } catch (IllegalArgumentException e) {
1432                throw new SAXException(tr("Unknown type: {0}", type), e);
1433            }
1434        }
1435        TYPE_CACHE.put(types, result);
1436        return result;
1437    }
1438
1439    static String fixPresetString(String s) {
1440        return s == null ? s : s.replaceAll("'","''");
1441    }
1442
1443    private static String getLocaleText(String text, String text_context, String defaultText) {
1444        if (text == null) {
1445            return defaultText;
1446        } else if (text_context != null) {
1447            return trc(text_context, fixPresetString(text));
1448        } else {
1449            return tr(fixPresetString(text));
1450        }
1451    }
1452
1453    /**
1454     * allow escaped comma in comma separated list:
1455     * "A\, B\, C,one\, two" --&gt; ["A, B, C", "one, two"]
1456     * @param delimiter the delimiter, e.g. a comma. separates the entries and
1457     *      must be escaped within one entry
1458     * @param s the string
1459     */
1460    private static String[] splitEscaped(char delimiter, String s) {
1461        if (s == null)
1462            return new String[0];
1463        List<String> result = new ArrayList<>();
1464        boolean backslash = false;
1465        StringBuilder item = new StringBuilder();
1466        for (int i=0; i<s.length(); i++) {
1467            char ch = s.charAt(i);
1468            if (backslash) {
1469                item.append(ch);
1470                backslash = false;
1471            } else if (ch == '\\') {
1472                backslash = true;
1473            } else if (ch == delimiter) {
1474                result.add(item.toString());
1475                item.setLength(0);
1476            } else {
1477                item.append(ch);
1478            }
1479        }
1480        if (item.length() > 0) {
1481            result.add(item.toString());
1482        }
1483        return result.toArray(new String[result.size()]);
1484    }
1485
1486    static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
1487        Usage returnValue = new Usage();
1488        returnValue.values = new TreeSet<>();
1489        for (OsmPrimitive s : sel) {
1490            String v = s.get(key);
1491            if (v != null) {
1492                returnValue.values.add(v);
1493            } else {
1494                returnValue.hadEmpty = true;
1495            }
1496            if(s.hasKeys()) {
1497                returnValue.hadKeys = true;
1498            }
1499        }
1500        return returnValue;
1501    }
1502
1503    static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
1504
1505        Usage returnValue = new Usage();
1506        returnValue.values = new TreeSet<>();
1507        for (OsmPrimitive s : sel) {
1508            String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
1509            if (booleanValue != null) {
1510                returnValue.values.add(booleanValue);
1511            }
1512        }
1513        return returnValue;
1514    }
1515
1516    protected static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) {
1517        final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null);
1518        ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true);
1519        if (maxSize != null) {
1520            imgProv.setMaxSize(maxSize);
1521        }
1522        return imgProv.get();
1523    }
1524
1525    protected static Integer parseInteger(String str) {
1526        if (str == null || str.isEmpty())
1527            return null;
1528        try {
1529            return Integer.parseInt(str);
1530        } catch (Exception e) {
1531            if (Main.isTraceEnabled()) {
1532                Main.trace(e.getMessage());
1533            }
1534        }
1535        return null;
1536    }
1537}