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