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