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

Last change on this file since 5208 was 5208, checked in by simon04, 12 years ago

fix #7637 - NullPointerException when parsing erroneous presets (<combo> w/o values)

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