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