001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.Font; 010import java.awt.GridBagLayout; 011import java.awt.GridLayout; 012import java.awt.event.ActionEvent; 013import java.awt.event.ActionListener; 014import java.awt.event.MouseAdapter; 015import java.awt.event.MouseEvent; 016import java.io.File; 017import java.lang.reflect.Method; 018import java.lang.reflect.Modifier; 019import java.text.NumberFormat; 020import java.text.ParseException; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.EnumSet; 026import java.util.HashMap; 027import java.util.LinkedHashMap; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.TreeSet; 032 033import javax.swing.ButtonGroup; 034import javax.swing.Icon; 035import javax.swing.ImageIcon; 036import javax.swing.JButton; 037import javax.swing.JComponent; 038import javax.swing.JLabel; 039import javax.swing.JList; 040import javax.swing.JPanel; 041import javax.swing.JScrollPane; 042import javax.swing.JSeparator; 043import javax.swing.JToggleButton; 044import javax.swing.ListCellRenderer; 045import javax.swing.ListModel; 046 047import org.openstreetmap.josm.Main; 048import org.openstreetmap.josm.actions.search.SearchCompiler; 049import org.openstreetmap.josm.data.osm.OsmPrimitive; 050import org.openstreetmap.josm.data.osm.OsmUtils; 051import org.openstreetmap.josm.data.osm.Tag; 052import org.openstreetmap.josm.data.preferences.BooleanProperty; 053import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField; 054import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionItemPriority; 055import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; 056import org.openstreetmap.josm.gui.widgets.JosmComboBox; 057import org.openstreetmap.josm.gui.widgets.JosmTextField; 058import org.openstreetmap.josm.gui.widgets.QuadStateCheckBox; 059import org.openstreetmap.josm.gui.widgets.UrlLabel; 060import org.openstreetmap.josm.tools.GBC; 061import org.openstreetmap.josm.tools.ImageProvider; 062import org.openstreetmap.josm.tools.Predicate; 063import org.openstreetmap.josm.tools.Utils; 064import org.xml.sax.SAXException; 065 066/** 067 * Class that contains all subtypes of TaggingPresetItem, static supplementary data, types and methods 068 * @since 6068 069 */ 070public final class TaggingPresetItems { 071 private TaggingPresetItems() { 072 } 073 074 private static int auto_increment_selected = 0; 075 /** Translatation of "<different>". Use in combo boxes to display en entry matching several different values. */ 076 public static final String DIFFERENT = tr("<different>"); 077 078 private static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false); 079 080 // 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) 081 private static final Map<String,EnumSet<TaggingPresetType>> TYPE_CACHE = new LinkedHashMap<>(16, 1.1f, true); 082 083 /** 084 * Last value of each key used in presets, used for prefilling corresponding fields 085 */ 086 private static final Map<String,String> LAST_VALUES = new HashMap<>(); 087 088 public static class PresetListEntry { 089 public String value; 090 /** The context used for translating {@link #value} */ 091 public String value_context; 092 public String display_value; 093 public String short_description; 094 /** The location of icon file to display */ 095 public String icon; 096 /** The size of displayed icon. If not set, default is size from icon file */ 097 public String icon_size; 098 /** The localized version of {@link #display_value}. */ 099 public String locale_display_value; 100 /** The localized version of {@link #short_description}. */ 101 public String locale_short_description; 102 private final File zipIcons = TaggingPresetReader.getZipIcons(); 103 104 // Cached size (currently only for Combo) to speed up preset dialog initialization 105 private int prefferedWidth = -1; 106 private int prefferedHeight = -1; 107 108 public String getListDisplay() { 109 if (value.equals(DIFFERENT)) 110 return "<b>"+DIFFERENT.replaceAll("<", "<").replaceAll(">", ">")+"</b>"; 111 112 if (value.isEmpty()) 113 return " "; 114 115 final StringBuilder res = new StringBuilder("<b>"); 116 res.append(getDisplayValue(true)); 117 res.append("</b>"); 118 if (getShortDescription(true) != null) { 119 // wrap in table to restrict the text width 120 res.append("<div style=\"width:300px; padding:0 0 5px 5px\">"); 121 res.append(getShortDescription(true)); 122 res.append("</div>"); 123 } 124 return res.toString(); 125 } 126 127 /** 128 * Returns the entry icon, if any. 129 * @return the entry icon, or {@code null} 130 */ 131 public ImageIcon getIcon() { 132 return icon == null ? null : loadImageIcon(icon, zipIcons, parseInteger(icon_size)); 133 } 134 135 /** 136 * Construxts a new {@code PresetListEntry}, uninitialized. 137 */ 138 public PresetListEntry() { 139 } 140 141 public PresetListEntry(String value) { 142 this.value = value; 143 } 144 145 public String getDisplayValue(boolean translated) { 146 return translated 147 ? Utils.firstNonNull(locale_display_value, tr(display_value), trc(value_context, value)) 148 : Utils.firstNonNull(display_value, value); 149 } 150 151 public String getShortDescription(boolean translated) { 152 return translated 153 ? Utils.firstNonNull(locale_short_description, tr(short_description)) 154 : short_description; 155 } 156 157 // toString is mainly used to initialize the Editor 158 @Override 159 public String toString() { 160 if (value.equals(DIFFERENT)) 161 return DIFFERENT; 162 return getDisplayValue(true).replaceAll("<.*>", ""); // remove additional markup, e.g. <br> 163 } 164 } 165 166 public static class Role { 167 public EnumSet<TaggingPresetType> types; 168 public String key; 169 /** The text to display */ 170 public String text; 171 /** The context used for translating {@link #text} */ 172 public String text_context; 173 /** The localized version of {@link #text}. */ 174 public String locale_text; 175 public SearchCompiler.Match memberExpression; 176 177 public boolean required = false; 178 private long count = 0; 179 180 public void setType(String types) throws SAXException { 181 this.types = getType(types); 182 } 183 184 public void setRequisite(String str) throws SAXException { 185 if("required".equals(str)) { 186 required = true; 187 } else if(!"optional".equals(str)) 188 throw new SAXException(tr("Unknown requisite: {0}", str)); 189 } 190 191 public void setMember_expression(String member_expression) throws SAXException { 192 try { 193 this.memberExpression = SearchCompiler.compile(member_expression, true, true); 194 } catch (SearchCompiler.ParseError ex) { 195 throw new SAXException(tr("Illegal member expression: {0}", ex.getMessage()), ex); 196 } 197 } 198 199 public void setCount(String count) { 200 this.count = Long.parseLong(count); 201 } 202 203 /** 204 * Return either argument, the highest possible value or the lowest allowed value 205 */ 206 public long getValidCount(long c) { 207 if (count > 0 && !required) 208 return c != 0 ? count : 0; 209 else if (count > 0) 210 return count; 211 else if (!required) 212 return c != 0 ? c : 0; 213 else 214 return c != 0 ? c : 1; 215 } 216 217 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 218 String cstring; 219 if (count > 0 && !required) { 220 cstring = "0,"+count; 221 } else if(count > 0) { 222 cstring = String.valueOf(count); 223 } else if(!required) { 224 cstring = "0-..."; 225 } else { 226 cstring = "1-..."; 227 } 228 if (locale_text == null) { 229 locale_text = getLocaleText(text, text_context, null); 230 } 231 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0)); 232 p.add(new JLabel(key), GBC.std().insets(0,0,10,0)); 233 p.add(new JLabel(cstring), types == null ? GBC.eol() : GBC.std().insets(0,0,10,0)); 234 if (types != null) { 235 JPanel pp = new JPanel(); 236 for(TaggingPresetType t : types) { 237 pp.add(new JLabel(ImageProvider.get(t.getIconName()))); 238 } 239 p.add(pp, GBC.eol()); 240 } 241 return true; 242 } 243 } 244 245 /** 246 * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed. 247 */ 248 public static enum MatchType { 249 250 /** Neutral, i.e., do not consider this item for matching. */ 251 NONE("none"), 252 /** Positive if key matches, neutral otherwise. */ 253 KEY("key"), 254 /** Positive if key matches, negative otherwise. */ 255 KEY_REQUIRED("key!"), 256 /** Positive if key and value matches, neutral otherwise. */ 257 KEY_VALUE("keyvalue"), 258 /** Positive if key and value matches, negative otherwise. */ 259 KEY_VALUE_REQUIRED("keyvalue!"); 260 261 private final String value; 262 263 private MatchType(String value) { 264 this.value = value; 265 } 266 267 /** 268 * Replies the associated textual value. 269 * @return the associated textual value 270 */ 271 public String getValue() { 272 return value; 273 } 274 275 /** 276 * Determines the {@code MatchType} for the given textual value. 277 * @param type the textual value 278 * @return the {@code MatchType} for the given textual value 279 */ 280 public static MatchType ofString(String type) { 281 for (MatchType i : EnumSet.allOf(MatchType.class)) { 282 if (i.getValue().equals(type)) 283 return i; 284 } 285 throw new IllegalArgumentException(type + " is not allowed"); 286 } 287 } 288 289 public static class Usage { 290 TreeSet<String> values; 291 boolean hadKeys = false; 292 boolean hadEmpty = false; 293 294 public boolean hasUniqueValue() { 295 return values.size() == 1 && !hadEmpty; 296 } 297 298 public boolean unused() { 299 return values.isEmpty(); 300 } 301 302 public String getFirst() { 303 return values.first(); 304 } 305 306 public boolean hadKeys() { 307 return hadKeys; 308 } 309 } 310 311 /** 312 * A tagging preset item displaying a localizable text. 313 * @since 6190 314 */ 315 public abstract static class TaggingPresetTextItem extends TaggingPresetItem { 316 317 /** The text to display */ 318 public String text; 319 320 /** The context used for translating {@link #text} */ 321 public String text_context; 322 323 /** The localized version of {@link #text} */ 324 public String locale_text; 325 326 protected final void initializeLocaleText(String defaultText) { 327 if (locale_text == null) { 328 locale_text = getLocaleText(text, text_context, defaultText); 329 } 330 } 331 332 @Override 333 void addCommands(List<Tag> changedTags) { 334 } 335 336 protected String fieldsToString() { 337 return (text != null ? "text=" + text + ", " : "") 338 + (text_context != null ? "text_context=" + text_context + ", " : "") 339 + (locale_text != null ? "locale_text=" + locale_text : ""); 340 } 341 342 @Override 343 public String toString() { 344 return getClass().getSimpleName() + " [" + fieldsToString() + "]"; 345 } 346 } 347 348 /** 349 * Label type. 350 */ 351 public static class Label extends TaggingPresetTextItem { 352 353 /** The location of icon file to display (optional) */ 354 public String icon; 355 /** The size of displayed icon. If not set, default is 16px */ 356 public String icon_size; 357 358 @Override 359 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 360 initializeLocaleText(null); 361 addLabel(p, getIcon(), locale_text); 362 return true; 363 } 364 365 /** 366 * Adds a new {@code JLabel} to the given panel. 367 * @param p The panel 368 * @param icon the icon (optional, can be null) 369 * @param label The text label 370 */ 371 public static void addLabel(JPanel p, Icon icon, String label) { 372 p.add(new JLabel(label, icon, JLabel.LEADING), GBC.eol().fill(GBC.HORIZONTAL)); 373 } 374 375 /** 376 * Returns the label icon, if any. 377 * @return the label icon, or {@code null} 378 */ 379 public ImageIcon getIcon() { 380 Integer size = parseInteger(icon_size); 381 return icon == null ? null : loadImageIcon(icon, TaggingPresetReader.getZipIcons(), size != null ? size : 16); 382 } 383 } 384 385 /** 386 * Hyperlink type. 387 */ 388 public static class Link extends TaggingPresetTextItem { 389 390 /** The link to display. */ 391 public String href; 392 393 /** The localized version of {@link #href}. */ 394 public String locale_href; 395 396 @Override 397 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 398 initializeLocaleText(tr("More information about this feature")); 399 String url = locale_href; 400 if (url == null) { 401 url = href; 402 } 403 if (url != null) { 404 p.add(new UrlLabel(url, locale_text, 2), GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL)); 405 } 406 return false; 407 } 408 409 @Override 410 protected String fieldsToString() { 411 return super.fieldsToString() 412 + (href != null ? "href=" + href + ", " : "") 413 + (locale_href != null ? "locale_href=" + locale_href + ", " : ""); 414 } 415 } 416 417 public static class PresetLink extends TaggingPresetItem { 418 419 public String preset_name = ""; 420 421 @Override 422 boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 423 final String presetName = preset_name; 424 final TaggingPreset t = Utils.filter(TaggingPresets.getTaggingPresets(), new Predicate<TaggingPreset>() { 425 @Override 426 public boolean evaluate(TaggingPreset object) { 427 return presetName.equals(object.name); 428 } 429 }).iterator().next(); 430 if (t == null) return false; 431 JLabel lbl = new PresetLabel(t); 432 lbl.addMouseListener(new MouseAdapter() { 433 @Override 434 public void mouseClicked(MouseEvent arg0) { 435 t.actionPerformed(null); 436 } 437 }); 438 p.add(lbl, GBC.eol().fill(GBC.HORIZONTAL)); 439 return false; 440 } 441 442 @Override 443 void addCommands(List<Tag> changedTags) { 444 } 445 } 446 447 public static class Roles extends TaggingPresetItem { 448 449 public final List<Role> roles = new LinkedList<>(); 450 451 @Override 452 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 453 p.add(new JLabel(" "), GBC.eol()); // space 454 if (!roles.isEmpty()) { 455 JPanel proles = new JPanel(new GridBagLayout()); 456 proles.add(new JLabel(tr("Available roles")), GBC.std().insets(0, 0, 10, 0)); 457 proles.add(new JLabel(tr("role")), GBC.std().insets(0, 0, 10, 0)); 458 proles.add(new JLabel(tr("count")), GBC.std().insets(0, 0, 10, 0)); 459 proles.add(new JLabel(tr("elements")), GBC.eol()); 460 for (Role i : roles) { 461 i.addToPanel(proles, sel); 462 } 463 p.add(proles, GBC.eol()); 464 } 465 return false; 466 } 467 468 @Override 469 public void addCommands(List<Tag> changedTags) { 470 } 471 } 472 473 public static class Optional extends TaggingPresetTextItem { 474 475 // TODO: Draw a box around optional stuff 476 @Override 477 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 478 initializeLocaleText(tr("Optional Attributes:")); 479 p.add(new JLabel(" "), GBC.eol()); // space 480 p.add(new JLabel(locale_text), GBC.eol()); 481 p.add(new JLabel(" "), GBC.eol()); // space 482 return false; 483 } 484 } 485 486 /** 487 * Horizontal separator type. 488 */ 489 public static class Space extends TaggingPresetItem { 490 491 @Override 492 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 493 p.add(new JLabel(" "), GBC.eol()); // space 494 return false; 495 } 496 497 @Override 498 public void addCommands(List<Tag> changedTags) { 499 } 500 501 @Override 502 public String toString() { 503 return "Space"; 504 } 505 } 506 507 /** 508 * Class used to represent a {@link JSeparator} inside tagging preset window. 509 * @since 6198 510 */ 511 public static class ItemSeparator extends TaggingPresetItem { 512 513 @Override 514 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 515 p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); 516 return false; 517 } 518 519 @Override 520 public void addCommands(List<Tag> changedTags) { 521 } 522 523 @Override 524 public String toString() { 525 return "ItemSeparator"; 526 } 527 } 528 529 /** 530 * Preset item associated to an OSM key. 531 */ 532 public abstract static class KeyedItem extends TaggingPresetItem { 533 534 public String key; 535 /** The text to display */ 536 public String text; 537 /** The context used for translating {@link #text} */ 538 public String text_context; 539 public String match = getDefaultMatch().getValue(); 540 541 public abstract MatchType getDefaultMatch(); 542 public abstract Collection<String> getValues(); 543 544 @Override 545 Boolean matches(Map<String, String> tags) { 546 switch (MatchType.ofString(match)) { 547 case NONE: 548 return null; 549 case KEY: 550 return tags.containsKey(key) ? true : null; 551 case KEY_REQUIRED: 552 return tags.containsKey(key); 553 case KEY_VALUE: 554 return tags.containsKey(key) && getValues().contains(tags.get(key)) ? true : null; 555 case KEY_VALUE_REQUIRED: 556 return tags.containsKey(key) && getValues().contains(tags.get(key)); 557 default: 558 throw new IllegalStateException(); 559 } 560 } 561 562 @Override 563 public String toString() { 564 return "KeyedItem [key=" + key + ", text=" + text 565 + ", text_context=" + text_context + ", match=" + match 566 + "]"; 567 } 568 } 569 570 /** 571 * Invisible type allowing to hardcode an OSM key/value from the preset definition. 572 */ 573 public static class Key extends KeyedItem { 574 575 /** The hardcoded value for key */ 576 public String value; 577 578 @Override 579 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 580 return false; 581 } 582 583 @Override 584 public void addCommands(List<Tag> changedTags) { 585 changedTags.add(new Tag(key, value)); 586 } 587 588 @Override 589 public MatchType getDefaultMatch() { 590 return MatchType.KEY_VALUE_REQUIRED; 591 } 592 593 @Override 594 public Collection<String> getValues() { 595 return Collections.singleton(value); 596 } 597 598 @Override 599 public String toString() { 600 return "Key [key=" + key + ", value=" + value + ", text=" + text 601 + ", text_context=" + text_context + ", match=" + match 602 + "]"; 603 } 604 } 605 606 /** 607 * Text field type. 608 */ 609 public static class Text extends KeyedItem { 610 611 /** The localized version of {@link #text}. */ 612 public String locale_text; 613 public String default_; 614 public String originalValue; 615 public String use_last_as_default = "false"; 616 public String auto_increment; 617 public String length; 618 public String alternative_autocomplete_keys; 619 620 private JComponent value; 621 622 @Override 623 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 624 625 // find out if our key is already used in the selection. 626 Usage usage = determineTextUsage(sel, key); 627 AutoCompletingTextField textField = new AutoCompletingTextField(); 628 if (alternative_autocomplete_keys != null) { 629 initAutoCompletionField(textField, (key + "," + alternative_autocomplete_keys).split(",")); 630 } else { 631 initAutoCompletionField(textField, key); 632 } 633 if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) { 634 textField.setHint(key); 635 } 636 if (length != null && !length.isEmpty()) { 637 textField.setMaxChars(Integer.valueOf(length)); 638 } 639 if (usage.unused()){ 640 if (auto_increment_selected != 0 && auto_increment != null) { 641 try { 642 textField.setText(Integer.toString(Integer.parseInt(LAST_VALUES.get(key)) + auto_increment_selected)); 643 } catch (NumberFormatException ex) { 644 // Ignore - cannot auto-increment if last was non-numeric 645 } 646 } 647 else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 648 // selected osm primitives are untagged or filling default values feature is enabled 649 if (!"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) { 650 textField.setText(LAST_VALUES.get(key)); 651 } else { 652 textField.setText(default_); 653 } 654 } else { 655 // selected osm primitives are tagged and filling default values feature is disabled 656 textField.setText(""); 657 } 658 value = textField; 659 originalValue = null; 660 } else if (usage.hasUniqueValue()) { 661 // all objects use the same value 662 textField.setText(usage.getFirst()); 663 value = textField; 664 originalValue = usage.getFirst(); 665 } else { 666 // the objects have different values 667 JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[0])); 668 comboBox.setEditable(true); 669 comboBox.setEditor(textField); 670 comboBox.getEditor().setItem(DIFFERENT); 671 value=comboBox; 672 originalValue = DIFFERENT; 673 } 674 if (locale_text == null) { 675 locale_text = getLocaleText(text, text_context, null); 676 } 677 678 // if there's an auto_increment setting, then wrap the text field 679 // into a panel, appending a number of buttons. 680 // auto_increment has a format like -2,-1,1,2 681 // the text box being the first component in the panel is relied 682 // on in a rather ugly fashion further down. 683 if (auto_increment != null) { 684 ButtonGroup bg = new ButtonGroup(); 685 JPanel pnl = new JPanel(new GridBagLayout()); 686 pnl.add(value, GBC.std().fill(GBC.HORIZONTAL)); 687 688 // first, one button for each auto_increment value 689 for (final String ai : auto_increment.split(",")) { 690 JToggleButton aibutton = new JToggleButton(ai); 691 aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai)); 692 aibutton.setMargin(new java.awt.Insets(0,0,0,0)); 693 aibutton.setFocusable(false); 694 bg.add(aibutton); 695 try { 696 // TODO there must be a better way to parse a number like "+3" than this. 697 final int buttonvalue = (NumberFormat.getIntegerInstance().parse(ai.replace("+", ""))).intValue(); 698 if (auto_increment_selected == buttonvalue) aibutton.setSelected(true); 699 aibutton.addActionListener(new ActionListener() { 700 @Override 701 public void actionPerformed(ActionEvent e) { 702 auto_increment_selected = buttonvalue; 703 } 704 }); 705 pnl.add(aibutton, GBC.std()); 706 } catch (ParseException x) { 707 Main.error("Cannot parse auto-increment value of '" + ai + "' into an integer"); 708 } 709 } 710 711 // an invisible toggle button for "release" of the button group 712 final JToggleButton clearbutton = new JToggleButton("X"); 713 clearbutton.setVisible(false); 714 clearbutton.setFocusable(false); 715 bg.add(clearbutton); 716 // and its visible counterpart. - this mechanism allows us to 717 // have *no* button selected after the X is clicked, instead 718 // of the X remaining selected 719 JButton releasebutton = new JButton("X"); 720 releasebutton.setToolTipText(tr("Cancel auto-increment for this field")); 721 releasebutton.setMargin(new java.awt.Insets(0,0,0,0)); 722 releasebutton.setFocusable(false); 723 releasebutton.addActionListener(new ActionListener() { 724 @Override 725 public void actionPerformed(ActionEvent e) { 726 auto_increment_selected = 0; 727 clearbutton.setSelected(true); 728 } 729 }); 730 pnl.add(releasebutton, GBC.eol()); 731 value = pnl; 732 } 733 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0)); 734 p.add(value, GBC.eol().fill(GBC.HORIZONTAL)); 735 return true; 736 } 737 738 private static String getValue(Component comp) { 739 if (comp instanceof JosmComboBox) { 740 return ((JosmComboBox<?>) comp).getEditor().getItem().toString(); 741 } else if (comp instanceof JosmTextField) { 742 return ((JosmTextField) comp).getText(); 743 } else if (comp instanceof JPanel) { 744 return getValue(((JPanel)comp).getComponent(0)); 745 } else { 746 return null; 747 } 748 } 749 750 @Override 751 public void addCommands(List<Tag> changedTags) { 752 753 // return if unchanged 754 String v = getValue(value); 755 if (v == null) { 756 Main.error("No 'last value' support for component " + value); 757 return; 758 } 759 760 v = Tag.removeWhiteSpaces(v); 761 762 if (!"false".equals(use_last_as_default) || auto_increment != null) { 763 LAST_VALUES.put(key, v); 764 } 765 if (v.equals(originalValue) || (originalValue == null && v.length() == 0)) 766 return; 767 768 changedTags.add(new Tag(key, v)); 769 } 770 771 @Override 772 boolean requestFocusInWindow() { 773 return value.requestFocusInWindow(); 774 } 775 776 @Override 777 public MatchType getDefaultMatch() { 778 return MatchType.NONE; 779 } 780 781 @Override 782 public Collection<String> getValues() { 783 if (default_ == null || default_.isEmpty()) 784 return Collections.emptyList(); 785 return Collections.singleton(default_); 786 } 787 } 788 789 /** 790 * A group of {@link Check}s. 791 * @since 6114 792 */ 793 public static class CheckGroup extends TaggingPresetItem { 794 795 /** 796 * Number of columns (positive integer) 797 */ 798 public String columns; 799 800 /** 801 * List of checkboxes 802 */ 803 public final List<Check> checks = new LinkedList<>(); 804 805 @Override 806 boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 807 Integer cols = Integer.valueOf(columns); 808 int rows = (int) Math.ceil(checks.size()/cols.doubleValue()); 809 JPanel panel = new JPanel(new GridLayout(rows, cols)); 810 811 for (Check check : checks) { 812 check.addToPanel(panel, sel, presetInitiallyMatches); 813 } 814 815 p.add(panel, GBC.eol()); 816 return false; 817 } 818 819 @Override 820 void addCommands(List<Tag> changedTags) { 821 for (Check check : checks) { 822 check.addCommands(changedTags); 823 } 824 } 825 826 @Override 827 public String toString() { 828 return "CheckGroup [columns=" + columns + "]"; 829 } 830 } 831 832 /** 833 * Checkbox type. 834 */ 835 public static class Check extends KeyedItem { 836 837 /** The localized version of {@link #text}. */ 838 public String locale_text; 839 /** the value to set when checked (default is "yes") */ 840 public String value_on = OsmUtils.trueval; 841 /** the value to set when unchecked (default is "no") */ 842 public String value_off = OsmUtils.falseval; 843 /** whether the off value is disabled in the dialog, i.e., only unset or yes are provided */ 844 public boolean disable_off = false; 845 /** ticked on/off (default is "off") */ 846 public boolean default_ = false; // only used for tagless objects 847 848 private QuadStateCheckBox check; 849 private QuadStateCheckBox.State initialState; 850 private boolean def; 851 852 @Override 853 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 854 855 // find out if our key is already used in the selection. 856 final Usage usage = determineBooleanUsage(sel, key); 857 final String oneValue = usage.values.isEmpty() ? null : usage.values.last(); 858 def = default_; 859 860 if (locale_text == null) { 861 locale_text = getLocaleText(text, text_context, null); 862 } 863 864 if (usage.values.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) { 865 if (def && !PROP_FILL_DEFAULT.get()) { 866 // default is set and filling default values feature is disabled - check if all primitives are untagged 867 for (OsmPrimitive s : sel) 868 if (s.hasKeys()) { 869 def = false; 870 } 871 } 872 873 // all selected objects share the same value which is either true or false or unset, 874 // we can display a standard check box. 875 initialState = value_on.equals(oneValue) 876 ? QuadStateCheckBox.State.SELECTED 877 : value_off.equals(oneValue) 878 ? QuadStateCheckBox.State.NOT_SELECTED 879 : def 880 ? QuadStateCheckBox.State.SELECTED 881 : QuadStateCheckBox.State.UNSET; 882 } else { 883 def = false; 884 // the objects have different values, or one or more objects have something 885 // else than true/false. we display a quad-state check box 886 // in "partial" state. 887 initialState = QuadStateCheckBox.State.PARTIAL; 888 } 889 890 final List<QuadStateCheckBox.State> allowedStates = new ArrayList<>(4); 891 if (QuadStateCheckBox.State.PARTIAL.equals(initialState)) 892 allowedStates.add(QuadStateCheckBox.State.PARTIAL); 893 allowedStates.add(QuadStateCheckBox.State.SELECTED); 894 if (!disable_off || value_off.equals(oneValue)) 895 allowedStates.add(QuadStateCheckBox.State.NOT_SELECTED); 896 allowedStates.add(QuadStateCheckBox.State.UNSET); 897 check = new QuadStateCheckBox(locale_text, initialState, 898 allowedStates.toArray(new QuadStateCheckBox.State[allowedStates.size()])); 899 900 p.add(check, GBC.eol().fill(GBC.HORIZONTAL)); 901 return true; 902 } 903 904 @Override 905 public void addCommands(List<Tag> changedTags) { 906 // if the user hasn't changed anything, don't create a command. 907 if (check.getState() == initialState && !def) return; 908 909 // otherwise change things according to the selected value. 910 changedTags.add(new Tag(key, 911 check.getState() == QuadStateCheckBox.State.SELECTED ? value_on : 912 check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? value_off : 913 null)); 914 } 915 916 @Override 917 boolean requestFocusInWindow() {return check.requestFocusInWindow();} 918 919 @Override 920 public MatchType getDefaultMatch() { 921 return MatchType.NONE; 922 } 923 924 @Override 925 public Collection<String> getValues() { 926 return disable_off ? Arrays.asList(value_on) : Arrays.asList(value_on, value_off); 927 } 928 929 @Override 930 public String toString() { 931 return "Check [" 932 + (locale_text != null ? "locale_text=" + locale_text + ", " : "") 933 + (value_on != null ? "value_on=" + value_on + ", " : "") 934 + (value_off != null ? "value_off=" + value_off + ", " : "") 935 + "default_=" + default_ + ", " 936 + (check != null ? "check=" + check + ", " : "") 937 + (initialState != null ? "initialState=" + initialState 938 + ", " : "") + "def=" + def + "]"; 939 } 940 } 941 942 /** 943 * Abstract superclass for combo box and multi-select list types. 944 */ 945 public abstract static class ComboMultiSelect extends KeyedItem { 946 947 /** The localized version of {@link #text}. */ 948 public String locale_text; 949 public String values; 950 public String values_from; 951 /** The context used for translating {@link #values} */ 952 public String values_context; 953 public String display_values; 954 /** The localized version of {@link #display_values}. */ 955 public String locale_display_values; 956 public String short_descriptions; 957 /** The localized version of {@link #short_descriptions}. */ 958 public String locale_short_descriptions; 959 public String default_; 960 public String delimiter = ";"; 961 public String use_last_as_default = "false"; 962 /** whether to use values for search via {@link TaggingPresetSelector} */ 963 public String values_searchable = "false"; 964 965 protected JComponent component; 966 protected final Map<String, PresetListEntry> lhm = new LinkedHashMap<>(); 967 private boolean initialized = false; 968 protected Usage usage; 969 protected Object originalValue; 970 971 protected abstract Object getSelectedItem(); 972 protected abstract void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches); 973 974 protected char getDelChar() { 975 return delimiter.isEmpty() ? ';' : delimiter.charAt(0); 976 } 977 978 @Override 979 public Collection<String> getValues() { 980 initListEntries(); 981 return lhm.keySet(); 982 } 983 984 public Collection<String> getDisplayValues() { 985 initListEntries(); 986 return Utils.transform(lhm.values(), new Utils.Function<PresetListEntry, String>() { 987 @Override 988 public String apply(PresetListEntry x) { 989 return x.getDisplayValue(true); 990 } 991 }); 992 } 993 994 @Override 995 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 996 997 initListEntries(); 998 999 // find out if our key is already used in the selection. 1000 usage = determineTextUsage(sel, key); 1001 if (!usage.hasUniqueValue() && !usage.unused()) { 1002 lhm.put(DIFFERENT, new PresetListEntry(DIFFERENT)); 1003 } 1004 1005 p.add(new JLabel(tr("{0}:", locale_text)), GBC.std().insets(0, 0, 10, 0)); 1006 addToPanelAnchor(p, default_, presetInitiallyMatches); 1007 1008 return true; 1009 1010 } 1011 1012 private void initListEntries() { 1013 if (initialized) { 1014 lhm.remove(DIFFERENT); // possibly added in #addToPanel 1015 return; 1016 } else if (lhm.isEmpty()) { 1017 initListEntriesFromAttributes(); 1018 } else { 1019 if (values != null) { 1020 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": " 1021 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.", 1022 key, text, "values", "list_entry")); 1023 } 1024 if (display_values != null || locale_display_values != null) { 1025 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": " 1026 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.", 1027 key, text, "display_values", "list_entry")); 1028 } 1029 if (short_descriptions != null || locale_short_descriptions != null) { 1030 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": " 1031 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.", 1032 key, text, "short_descriptions", "list_entry")); 1033 } 1034 for (PresetListEntry e : lhm.values()) { 1035 if (e.value_context == null) { 1036 e.value_context = values_context; 1037 } 1038 } 1039 } 1040 if (locale_text == null) { 1041 locale_text = getLocaleText(text, text_context, null); 1042 } 1043 initialized = true; 1044 } 1045 1046 private String[] initListEntriesFromAttributes() { 1047 char delChar = getDelChar(); 1048 1049 String[] value_array = null; 1050 1051 if (values_from != null) { 1052 String[] class_method = values_from.split("#"); 1053 if (class_method != null && class_method.length == 2) { 1054 try { 1055 Method method = Class.forName(class_method[0]).getMethod(class_method[1]); 1056 // Check method is public static String[] methodName() 1057 int mod = method.getModifiers(); 1058 if (Modifier.isPublic(mod) && Modifier.isStatic(mod) 1059 && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) { 1060 value_array = (String[]) method.invoke(null); 1061 } else { 1062 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text, 1063 "public static String[] methodName()")); 1064 } 1065 } catch (Exception e) { 1066 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text, 1067 e.getClass().getName(), e.getMessage())); 1068 } 1069 } 1070 } 1071 1072 if (value_array == null) { 1073 value_array = splitEscaped(delChar, values); 1074 } 1075 1076 final String displ = Utils.firstNonNull(locale_display_values, display_values); 1077 String[] display_array = displ == null ? value_array : splitEscaped(delChar, displ); 1078 1079 final String descr = Utils.firstNonNull(locale_short_descriptions, short_descriptions); 1080 String[] short_descriptions_array = descr == null ? null : splitEscaped(delChar, descr); 1081 1082 if (display_array.length != value_array.length) { 1083 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''display_values'' must be the same as in ''values''", key, text)); 1084 display_array = value_array; 1085 } 1086 1087 if (short_descriptions_array != null && short_descriptions_array.length != value_array.length) { 1088 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''short_descriptions'' must be the same as in ''values''", key, text)); 1089 short_descriptions_array = null; 1090 } 1091 1092 for (int i = 0; i < value_array.length; i++) { 1093 final PresetListEntry e = new PresetListEntry(value_array[i]); 1094 e.locale_display_value = locale_display_values != null 1095 ? display_array[i] 1096 : trc(values_context, fixPresetString(display_array[i])); 1097 if (short_descriptions_array != null) { 1098 e.locale_short_description = locale_short_descriptions != null 1099 ? short_descriptions_array[i] 1100 : tr(fixPresetString(short_descriptions_array[i])); 1101 } 1102 lhm.put(value_array[i], e); 1103 display_array[i] = e.getDisplayValue(true); 1104 } 1105 1106 return display_array; 1107 } 1108 1109 protected String getDisplayIfNull() { 1110 return null; 1111 } 1112 1113 @Override 1114 public void addCommands(List<Tag> changedTags) { 1115 Object obj = getSelectedItem(); 1116 String display = (obj == null) ? null : obj.toString(); 1117 String value = null; 1118 if (display == null) { 1119 display = getDisplayIfNull(); 1120 } 1121 1122 if (display != null) { 1123 for (String val : lhm.keySet()) { 1124 String k = lhm.get(val).toString(); 1125 if (k != null && k.equals(display)) { 1126 value = val; 1127 break; 1128 } 1129 } 1130 if (value == null) { 1131 value = display; 1132 } 1133 } else { 1134 value = ""; 1135 } 1136 value = Tag.removeWhiteSpaces(value); 1137 1138 // no change if same as before 1139 if (originalValue == null) { 1140 if (value.length() == 0) 1141 return; 1142 } else if (value.equals(originalValue.toString())) 1143 return; 1144 1145 if (!"false".equals(use_last_as_default)) { 1146 LAST_VALUES.put(key, value); 1147 } 1148 changedTags.add(new Tag(key, value)); 1149 } 1150 1151 public void addListEntry(PresetListEntry e) { 1152 lhm.put(e.value, e); 1153 } 1154 1155 public void addListEntries(Collection<PresetListEntry> e) { 1156 for (PresetListEntry i : e) { 1157 addListEntry(i); 1158 } 1159 } 1160 1161 @Override 1162 boolean requestFocusInWindow() { 1163 return component.requestFocusInWindow(); 1164 } 1165 1166 private static final ListCellRenderer<PresetListEntry> RENDERER = new ListCellRenderer<PresetListEntry>() { 1167 1168 JLabel lbl = new JLabel(); 1169 1170 @Override 1171 public Component getListCellRendererComponent( 1172 JList<? extends PresetListEntry> list, 1173 PresetListEntry item, 1174 int index, 1175 boolean isSelected, 1176 boolean cellHasFocus) { 1177 1178 // Only return cached size, item is not shown 1179 if (!list.isShowing() && item.prefferedWidth != -1 && item.prefferedHeight != -1) { 1180 if (index == -1) { 1181 lbl.setPreferredSize(new Dimension(item.prefferedWidth, 10)); 1182 } else { 1183 lbl.setPreferredSize(new Dimension(item.prefferedWidth, item.prefferedHeight)); 1184 } 1185 return lbl; 1186 } 1187 1188 lbl.setPreferredSize(null); 1189 1190 1191 if (isSelected) { 1192 lbl.setBackground(list.getSelectionBackground()); 1193 lbl.setForeground(list.getSelectionForeground()); 1194 } else { 1195 lbl.setBackground(list.getBackground()); 1196 lbl.setForeground(list.getForeground()); 1197 } 1198 1199 lbl.setOpaque(true); 1200 lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN)); 1201 lbl.setText("<html>" + item.getListDisplay() + "</html>"); 1202 lbl.setIcon(item.getIcon()); 1203 lbl.setEnabled(list.isEnabled()); 1204 1205 // Cache size 1206 item.prefferedWidth = lbl.getPreferredSize().width; 1207 item.prefferedHeight = lbl.getPreferredSize().height; 1208 1209 // We do not want the editor to have the maximum height of all 1210 // entries. Return a dummy with bogus height. 1211 if (index == -1) { 1212 lbl.setPreferredSize(new Dimension(lbl.getPreferredSize().width, 10)); 1213 } 1214 return lbl; 1215 } 1216 }; 1217 1218 protected ListCellRenderer<PresetListEntry> getListCellRenderer() { 1219 return RENDERER; 1220 } 1221 1222 @Override 1223 public MatchType getDefaultMatch() { 1224 return MatchType.NONE; 1225 } 1226 } 1227 1228 /** 1229 * Combobox type. 1230 */ 1231 public static class Combo extends ComboMultiSelect { 1232 1233 public boolean editable = true; 1234 protected JosmComboBox<PresetListEntry> combo; 1235 public String length; 1236 1237 /** 1238 * Constructs a new {@code Combo}. 1239 */ 1240 public Combo() { 1241 delimiter = ","; 1242 } 1243 1244 @Override 1245 protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) { 1246 if (!usage.unused()) { 1247 for (String s : usage.values) { 1248 if (!lhm.containsKey(s)) { 1249 lhm.put(s, new PresetListEntry(s)); 1250 } 1251 } 1252 } 1253 if (def != null && !lhm.containsKey(def)) { 1254 lhm.put(def, new PresetListEntry(def)); 1255 } 1256 lhm.put("", new PresetListEntry("")); 1257 1258 combo = new JosmComboBox<>(lhm.values().toArray(new PresetListEntry[0])); 1259 component = combo; 1260 combo.setRenderer(getListCellRenderer()); 1261 combo.setEditable(editable); 1262 combo.reinitialize(lhm.values()); 1263 AutoCompletingTextField tf = new AutoCompletingTextField(); 1264 initAutoCompletionField(tf, key); 1265 if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) { 1266 tf.setHint(key); 1267 } 1268 if (length != null && !length.isEmpty()) { 1269 tf.setMaxChars(Integer.valueOf(length)); 1270 } 1271 AutoCompletionList acList = tf.getAutoCompletionList(); 1272 if (acList != null) { 1273 acList.add(getDisplayValues(), AutoCompletionItemPriority.IS_IN_STANDARD); 1274 } 1275 combo.setEditor(tf); 1276 1277 if (usage.hasUniqueValue()) { 1278 // all items have the same value (and there were no unset items) 1279 originalValue = lhm.get(usage.getFirst()); 1280 combo.setSelectedItem(originalValue); 1281 } else if (def != null && usage.unused()) { 1282 // default is set and all items were unset 1283 if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 1284 // selected osm primitives are untagged or filling default feature is enabled 1285 combo.setSelectedItem(lhm.get(def).getDisplayValue(true)); 1286 } else { 1287 // selected osm primitives are tagged and filling default feature is disabled 1288 combo.setSelectedItem(""); 1289 } 1290 originalValue = lhm.get(DIFFERENT); 1291 } else if (usage.unused()) { 1292 // all items were unset (and so is default) 1293 originalValue = lhm.get(""); 1294 if ("force".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) { 1295 combo.setSelectedItem(lhm.get(LAST_VALUES.get(key))); 1296 } else { 1297 combo.setSelectedItem(originalValue); 1298 } 1299 } else { 1300 originalValue = lhm.get(DIFFERENT); 1301 combo.setSelectedItem(originalValue); 1302 } 1303 p.add(combo, GBC.eol().fill(GBC.HORIZONTAL)); 1304 1305 } 1306 1307 @Override 1308 protected Object getSelectedItem() { 1309 return combo.getSelectedItem(); 1310 1311 } 1312 1313 @Override 1314 protected String getDisplayIfNull() { 1315 if (combo.isEditable()) 1316 return combo.getEditor().getItem().toString(); 1317 else 1318 return null; 1319 } 1320 } 1321 1322 /** 1323 * Multi-select list type. 1324 */ 1325 public static class MultiSelect extends ComboMultiSelect { 1326 1327 public long rows = -1; 1328 protected ConcatenatingJList list; 1329 1330 @Override 1331 protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) { 1332 list = new ConcatenatingJList(delimiter, lhm.values().toArray(new PresetListEntry[0])); 1333 component = list; 1334 ListCellRenderer<PresetListEntry> renderer = getListCellRenderer(); 1335 list.setCellRenderer(renderer); 1336 1337 if (usage.hasUniqueValue() && !usage.unused()) { 1338 originalValue = usage.getFirst(); 1339 list.setSelectedItem(originalValue); 1340 } else if (def != null && !usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 1341 originalValue = DIFFERENT; 1342 list.setSelectedItem(def); 1343 } else if (usage.unused()) { 1344 originalValue = null; 1345 list.setSelectedItem(originalValue); 1346 } else { 1347 originalValue = DIFFERENT; 1348 list.setSelectedItem(originalValue); 1349 } 1350 1351 JScrollPane sp = new JScrollPane(list); 1352 // if a number of rows has been specified in the preset, 1353 // modify preferred height of scroll pane to match that row count. 1354 if (rows != -1) { 1355 double height = renderer.getListCellRendererComponent(list, 1356 new PresetListEntry("x"), 0, false, false).getPreferredSize().getHeight() * rows; 1357 sp.setPreferredSize(new Dimension((int) sp.getPreferredSize().getWidth(), (int) height)); 1358 } 1359 p.add(sp, GBC.eol().fill(GBC.HORIZONTAL)); 1360 } 1361 1362 @Override 1363 protected Object getSelectedItem() { 1364 return list.getSelectedItem(); 1365 } 1366 1367 @Override 1368 public void addCommands(List<Tag> changedTags) { 1369 // Do not create any commands if list has been disabled because of an unknown value (fix #8605) 1370 if (list.isEnabled()) { 1371 super.addCommands(changedTags); 1372 } 1373 } 1374 } 1375 1376 /** 1377 * Class that allows list values to be assigned and retrieved as a comma-delimited 1378 * string (extracted from TaggingPreset) 1379 */ 1380 private static class ConcatenatingJList extends JList<PresetListEntry> { 1381 private String delimiter; 1382 public ConcatenatingJList(String del, PresetListEntry[] o) { 1383 super(o); 1384 delimiter = del; 1385 } 1386 1387 public void setSelectedItem(Object o) { 1388 if (o == null) { 1389 clearSelection(); 1390 } else { 1391 String s = o.toString(); 1392 TreeSet<String> parts = new TreeSet<>(Arrays.asList(s.split(delimiter))); 1393 ListModel<PresetListEntry> lm = getModel(); 1394 int[] intParts = new int[lm.getSize()]; 1395 int j = 0; 1396 for (int i = 0; i < lm.getSize(); i++) { 1397 if (parts.contains((lm.getElementAt(i).value))) { 1398 intParts[j++]=i; 1399 } 1400 } 1401 setSelectedIndices(Arrays.copyOf(intParts, j)); 1402 // check if we have actually managed to represent the full 1403 // value with our presets. if not, cop out; we will not offer 1404 // a selection list that threatens to ruin the value. 1405 setEnabled(Utils.join(delimiter, parts).equals(getSelectedItem())); 1406 } 1407 } 1408 1409 public String getSelectedItem() { 1410 ListModel<PresetListEntry> lm = getModel(); 1411 int[] si = getSelectedIndices(); 1412 StringBuilder builder = new StringBuilder(); 1413 for (int i=0; i<si.length; i++) { 1414 if (i>0) { 1415 builder.append(delimiter); 1416 } 1417 builder.append(lm.getElementAt(si[i]).value); 1418 } 1419 return builder.toString(); 1420 } 1421 } 1422 1423 public static EnumSet<TaggingPresetType> getType(String types) throws SAXException { 1424 if (TYPE_CACHE.containsKey(types)) 1425 return TYPE_CACHE.get(types); 1426 EnumSet<TaggingPresetType> result = EnumSet.noneOf(TaggingPresetType.class); 1427 for (String type : Arrays.asList(types.split(","))) { 1428 try { 1429 TaggingPresetType presetType = TaggingPresetType.fromString(type); 1430 result.add(presetType); 1431 } catch (IllegalArgumentException e) { 1432 throw new SAXException(tr("Unknown type: {0}", type), e); 1433 } 1434 } 1435 TYPE_CACHE.put(types, result); 1436 return result; 1437 } 1438 1439 static String fixPresetString(String s) { 1440 return s == null ? s : s.replaceAll("'","''"); 1441 } 1442 1443 private static String getLocaleText(String text, String text_context, String defaultText) { 1444 if (text == null) { 1445 return defaultText; 1446 } else if (text_context != null) { 1447 return trc(text_context, fixPresetString(text)); 1448 } else { 1449 return tr(fixPresetString(text)); 1450 } 1451 } 1452 1453 /** 1454 * allow escaped comma in comma separated list: 1455 * "A\, B\, C,one\, two" --> ["A, B, C", "one, two"] 1456 * @param delimiter the delimiter, e.g. a comma. separates the entries and 1457 * must be escaped within one entry 1458 * @param s the string 1459 */ 1460 private static String[] splitEscaped(char delimiter, String s) { 1461 if (s == null) 1462 return new String[0]; 1463 List<String> result = new ArrayList<>(); 1464 boolean backslash = false; 1465 StringBuilder item = new StringBuilder(); 1466 for (int i=0; i<s.length(); i++) { 1467 char ch = s.charAt(i); 1468 if (backslash) { 1469 item.append(ch); 1470 backslash = false; 1471 } else if (ch == '\\') { 1472 backslash = true; 1473 } else if (ch == delimiter) { 1474 result.add(item.toString()); 1475 item.setLength(0); 1476 } else { 1477 item.append(ch); 1478 } 1479 } 1480 if (item.length() > 0) { 1481 result.add(item.toString()); 1482 } 1483 return result.toArray(new String[result.size()]); 1484 } 1485 1486 static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) { 1487 Usage returnValue = new Usage(); 1488 returnValue.values = new TreeSet<>(); 1489 for (OsmPrimitive s : sel) { 1490 String v = s.get(key); 1491 if (v != null) { 1492 returnValue.values.add(v); 1493 } else { 1494 returnValue.hadEmpty = true; 1495 } 1496 if(s.hasKeys()) { 1497 returnValue.hadKeys = true; 1498 } 1499 } 1500 return returnValue; 1501 } 1502 1503 static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) { 1504 1505 Usage returnValue = new Usage(); 1506 returnValue.values = new TreeSet<>(); 1507 for (OsmPrimitive s : sel) { 1508 String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key)); 1509 if (booleanValue != null) { 1510 returnValue.values.add(booleanValue); 1511 } 1512 } 1513 return returnValue; 1514 } 1515 1516 protected static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) { 1517 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null); 1518 ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true); 1519 if (maxSize != null) { 1520 imgProv.setMaxSize(maxSize); 1521 } 1522 return imgProv.get(); 1523 } 1524 1525 protected static Integer parseInteger(String str) { 1526 if (str == null || str.isEmpty()) 1527 return null; 1528 try { 1529 return Integer.parseInt(str); 1530 } catch (Exception e) { 1531 if (Main.isTraceEnabled()) { 1532 Main.trace(e.getMessage()); 1533 } 1534 } 1535 return null; 1536 } 1537}