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