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