source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPreset.java@ 5931

Last change on this file since 5931 was 5931, checked in by Don-vip, 11 years ago

fix #8664 - Display preset icon as preset dialog's frame icon

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