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

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

fix #8737 - Presets: add length attribute for combos

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