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