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, 24); 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.reinitialize(lhm.values()); 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 if ("force".equals(use_last_as_default) && lastValue.containsKey(key)) { 865 combo.setSelectedItem(lhm.get(lastValue.get(key))); 866 } else { 867 combo.setSelectedItem(originalValue); 868 } 869 } else { 870 originalValue = lhm.get(DIFFERENT); 871 combo.setSelectedItem(originalValue); 872 } 873 p.add(combo, GBC.eol().fill(GBC.HORIZONTAL)); 874 875 } 876 877 @Override 878 protected Object getSelectedItem() { 879 return combo.getSelectedItem(); 880 881 } 882 883 @Override 884 protected String getDisplayIfNull(String display) { 885 if (combo.isEditable()) 886 return combo.getEditor().getItem().toString(); 887 else 888 return display; 889 890 } 891 } 892 893 /** 894 * Class that allows list values to be assigned and retrieved as a comma-delimited 895 * string. 896 */ 897 public static class ConcatenatingJList extends JList { 898 private String delimiter; 899 public ConcatenatingJList(String del, Object[] o) { 900 super(o); 901 delimiter = del; 902 } 903 public void setSelectedItem(Object o) { 904 if (o == null) { 905 clearSelection(); 906 } else { 907 String s = o.toString(); 908 HashSet<String> parts = new HashSet<String>(Arrays.asList(s.split(delimiter))); 909 ListModel lm = getModel(); 910 int[] intParts = new int[lm.getSize()]; 911 int j = 0; 912 for (int i = 0; i < lm.getSize(); i++) { 913 if (parts.contains((((PresetListEntry)lm.getElementAt(i)).value))) { 914 intParts[j++]=i; 915 } 916 } 917 setSelectedIndices(Arrays.copyOf(intParts, j)); 918 // check if we have actually managed to represent the full 919 // value with our presets. if not, cop out; we will not offer 920 // a selection list that threatens to ruin the value. 921 setEnabled(s.equals(getSelectedItem())); 922 } 923 } 924 public String getSelectedItem() { 925 ListModel lm = getModel(); 926 int[] si = getSelectedIndices(); 927 StringBuilder builder = new StringBuilder(); 928 for (int i=0; i<si.length; i++) { 929 if (i>0) { 930 builder.append(delimiter); 931 } 932 builder.append(((PresetListEntry)lm.getElementAt(si[i])).value); 933 } 934 return builder.toString(); 935 } 936 } 937 938 public static class MultiSelect extends ComboMultiSelect { 939 940 public long rows = -1; 941 protected ConcatenatingJList list; 942 943 @Override 944 protected void addToPanelAnchor(JPanel p, String def) { 945 list = new ConcatenatingJList(delimiter, lhm.values().toArray()); 946 component = list; 947 ListCellRenderer renderer = getListCellRenderer(); 948 list.setCellRenderer(renderer); 949 950 if (usage.hasUniqueValue() && !usage.unused()) { 951 originalValue = usage.getFirst(); 952 list.setSelectedItem(originalValue); 953 } else if (def != null && !usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 954 originalValue = DIFFERENT; 955 list.setSelectedItem(def); 956 } else if (usage.unused()) { 957 originalValue = null; 958 list.setSelectedItem(originalValue); 959 } else { 960 originalValue = DIFFERENT; 961 list.setSelectedItem(originalValue); 962 } 963 964 JScrollPane sp = new JScrollPane(list); 965 // if a number of rows has been specified in the preset, 966 // modify preferred height of scroll pane to match that row count. 967 if (rows != -1) { 968 double height = renderer.getListCellRendererComponent(list, 969 new PresetListEntry("x"), 0, false, false).getPreferredSize().getHeight() * rows; 970 sp.setPreferredSize(new Dimension((int) sp.getPreferredSize().getWidth(), (int) height)); 971 } 972 p.add(sp, GBC.eol().fill(GBC.HORIZONTAL)); 973 974 975 } 976 977 @Override 978 protected Object getSelectedItem() { 979 return list.getSelectedItem(); 980 } 981 } 982 983 /** 984 * allow escaped comma in comma separated list: 985 * "A\, B\, C,one\, two" --> ["A, B, C", "one, two"] 986 * @param delimiter the delimiter, e.g. a comma. separates the entries and 987 * must be escaped within one entry 988 * @param s the string 989 */ 990 private static String[] splitEscaped(char delimiter, String s) { 991 if (s == null) 992 return new String[0]; 993 List<String> result = new ArrayList<String>(); 994 boolean backslash = false; 995 StringBuilder item = new StringBuilder(); 996 for (int i=0; i<s.length(); i++) { 997 char ch = s.charAt(i); 998 if (backslash) { 999 item.append(ch); 1000 backslash = false; 1001 } else if (ch == '\\') { 1002 backslash = true; 1003 } else if (ch == delimiter) { 1004 result.add(item.toString()); 1005 item.setLength(0); 1006 } else { 1007 item.append(ch); 1008 } 1009 } 1010 if (item.length() > 0) { 1011 result.add(item.toString()); 1012 } 1013 return result.toArray(new String[result.size()]); 1014 } 1015 1016 public static class Label extends Item { 1017 1018 public String text; 1019 public String text_context; 1020 public String locale_text; 1021 1022 @Override 1023 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 1024 if (locale_text == null) { 1025 if (text_context != null) { 1026 locale_text = trc(text_context, fixPresetString(text)); 1027 } else { 1028 locale_text = tr(fixPresetString(text)); 1029 } 1030 } 1031 p.add(new JLabel(locale_text), GBC.eol()); 1032 return false; 1033 } 1034 1035 @Override 1036 public void addCommands(List<Tag> changedTags) { 1037 } 1038 } 1039 1040 public static class Link extends Item { 1041 1042 public String href; 1043 public String text; 1044 public String text_context; 1045 public String locale_text; 1046 public String locale_href; 1047 1048 @Override 1049 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 1050 if (locale_text == null) { 1051 if (text == null) { 1052 locale_text = tr("More information about this feature"); 1053 } else if (text_context != null) { 1054 locale_text = trc(text_context, fixPresetString(text)); 1055 } else { 1056 locale_text = tr(fixPresetString(text)); 1057 } 1058 } 1059 String url = locale_href; 1060 if (url == null) { 1061 url = href; 1062 } 1063 if (url != null) { 1064 p.add(new UrlLabel(url, locale_text, 2), GBC.eol().anchor(GBC.WEST)); 1065 } 1066 return false; 1067 } 1068 1069 @Override 1070 public void addCommands(List<Tag> changedTags) { 1071 } 1072 } 1073 1074 public static class Role { 1075 public EnumSet<PresetType> types; 1076 public String key; 1077 public String text; 1078 public String text_context; 1079 public String locale_text; 1080 1081 public boolean required = false; 1082 public long count = 0; 1083 1084 public void setType(String types) throws SAXException { 1085 this.types = TaggingPreset.getType(types); 1086 } 1087 1088 public void setRequisite(String str) throws SAXException { 1089 if("required".equals(str)) { 1090 required = true; 1091 } else if(!"optional".equals(str)) 1092 throw new SAXException(tr("Unknown requisite: {0}", str)); 1093 } 1094 1095 /* return either argument, the highest possible value or the lowest 1096 allowed value */ 1097 public long getValidCount(long c) 1098 { 1099 if(count > 0 && !required) 1100 return c != 0 ? count : 0; 1101 else if(count > 0) 1102 return count; 1103 else if(!required) 1104 return c != 0 ? c : 0; 1105 else 1106 return c != 0 ? c : 1; 1107 } 1108 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 1109 String cstring; 1110 if(count > 0 && !required) { 1111 cstring = "0,"+String.valueOf(count); 1112 } else if(count > 0) { 1113 cstring = String.valueOf(count); 1114 } else if(!required) { 1115 cstring = "0-..."; 1116 } else { 1117 cstring = "1-..."; 1118 } 1119 if(locale_text == null) { 1120 if (text != null) { 1121 if(text_context != null) { 1122 locale_text = trc(text_context, fixPresetString(text)); 1123 } else { 1124 locale_text = tr(fixPresetString(text)); 1125 } 1126 } 1127 } 1128 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0)); 1129 p.add(new JLabel(key), GBC.std().insets(0,0,10,0)); 1130 p.add(new JLabel(cstring), types == null ? GBC.eol() : GBC.std().insets(0,0,10,0)); 1131 if(types != null){ 1132 JPanel pp = new JPanel(); 1133 for(PresetType t : types) { 1134 pp.add(new JLabel(ImageProvider.get(t.getIconName()))); 1135 } 1136 p.add(pp, GBC.eol()); 1137 } 1138 return true; 1139 } 1140 } 1141 1142 public static class Roles extends Item { 1143 1144 public List<Role> roles = new LinkedList<Role>(); 1145 1146 @Override 1147 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 1148 p.add(new JLabel(" "), GBC.eol()); // space 1149 if (roles.size() > 0) { 1150 JPanel proles = new JPanel(new GridBagLayout()); 1151 proles.add(new JLabel(tr("Available roles")), GBC.std().insets(0, 0, 10, 0)); 1152 proles.add(new JLabel(tr("role")), GBC.std().insets(0, 0, 10, 0)); 1153 proles.add(new JLabel(tr("count")), GBC.std().insets(0, 0, 10, 0)); 1154 proles.add(new JLabel(tr("elements")), GBC.eol()); 1155 for (Role i : roles) { 1156 i.addToPanel(proles, sel); 1157 } 1158 p.add(proles, GBC.eol()); 1159 } 1160 return false; 1161 } 1162 1163 @Override 1164 public void addCommands(List<Tag> changedTags) { 1165 } 1166 } 1167 1168 public static class Optional extends Item { 1169 1170 // TODO: Draw a box around optional stuff 1171 @Override 1172 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 1173 p.add(new JLabel(" "), GBC.eol()); // space 1174 p.add(new JLabel(tr("Optional Attributes:")), GBC.eol()); 1175 p.add(new JLabel(" "), GBC.eol()); // space 1176 return false; 1177 } 1178 1179 @Override 1180 public void addCommands(List<Tag> changedTags) { 1181 } 1182 } 1183 1184 public static class Space extends Item { 1185 1186 @Override 1187 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 1188 p.add(new JLabel(" "), GBC.eol()); // space 1189 return false; 1190 } 1191 1192 @Override 1193 public void addCommands(List<Tag> changedTags) { 1194 } 1195 } 1196 1197 public static class Key extends KeyedItem { 1198 1199 public String value; 1200 1201 @Override 1202 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 1203 return false; 1204 } 1205 1206 @Override 1207 public void addCommands(List<Tag> changedTags) { 1208 changedTags.add(new Tag(key, value)); 1209 } 1210 1211 @Override 1212 public MatchType getDefaultMatch() { 1213 return MatchType.KEY_VALUE; 1214 } 1215 1216 @Override 1217 public Collection<String> getValues() { 1218 return Collections.singleton(value); 1219 } 1220 } 1221 1222 /** 1223 * The types as preparsed collection. 1224 */ 1225 public EnumSet<PresetType> types; 1226 public List<Item> data = new LinkedList<Item>(); 1227 public TemplateEntry nameTemplate; 1228 public Match nameTemplateFilter; 1229 private static final HashMap<String,String> lastValue = new HashMap<String,String>(); 1230 1231 /** 1232 * Create an empty tagging preset. This will not have any items and 1233 * will be an empty string as text. createPanel will return null. 1234 * Use this as default item for "do not select anything". 1235 */ 1236 public TaggingPreset() { 1237 MapView.addLayerChangeListener(this); 1238 updateEnabledState(); 1239 } 1240 1241 /** 1242 * Change the display name without changing the toolbar value. 1243 */ 1244 public void setDisplayName() { 1245 putValue(Action.NAME, getName()); 1246 putValue("toolbar", "tagging_" + getRawName()); 1247 putValue(OPTIONAL_TOOLTIP_TEXT, (group != null ? 1248 tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) : 1249 tr("Use preset ''{0}''", getLocaleName()))); 1250 } 1251 1252 public String getLocaleName() { 1253 if(locale_name == null) { 1254 if(name_context != null) { 1255 locale_name = trc(name_context, fixPresetString(name)); 1256 } else { 1257 locale_name = tr(fixPresetString(name)); 1258 } 1259 } 1260 return locale_name; 1261 } 1262 1263 public String getName() { 1264 return group != null ? group.getName() + "/" + getLocaleName() : getLocaleName(); 1265 } 1266 public String getRawName() { 1267 return group != null ? group.getRawName() + "/" + name : name; 1268 } 1269 1270 protected static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) { 1271 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null); 1272 ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true); 1273 if (maxSize != null) { 1274 imgProv.setMaxSize(24); 1275 } 1276 return imgProv.get(); 1277 } 1278 1279 /* 1280 * Called from the XML parser to set the icon. 1281 * This task is performed in the background in order to speedup startup. 1282 * 1283 * FIXME for Java 1.6 - use 24x24 icons for LARGE_ICON_KEY (button bar) 1284 * and the 16x16 icons for SMALL_ICON. 1285 */ 1286 public void setIcon(final String iconName) { 1287 ImageProvider imgProv = new ImageProvider(iconName); 1288 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null); 1289 imgProv.setDirs(s); 1290 imgProv.setId("presets"); 1291 imgProv.setArchive(TaggingPreset.zipIcons); 1292 imgProv.setOptional(true); 1293 imgProv.setMaxWidth(16).setMaxHeight(16); 1294 imgProv.getInBackground(new ImageProvider.ImageCallback() { 1295 @Override 1296 public void finished(final ImageIcon result) { 1297 if (result != null) { 1298 GuiHelper.runInEDT(new Runnable() { 1299 @Override 1300 public void run() { 1301 putValue(Action.SMALL_ICON, result); 1302 } 1303 }); 1304 } else { 1305 System.out.println("Could not get presets icon " + iconName); 1306 } 1307 } 1308 }); 1309 } 1310 1311 // 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) 1312 private static final Map<String,EnumSet<PresetType>> typeCache = 1313 new LinkedHashMap<String, EnumSet<PresetType>>(16, 1.1f, true); 1314 1315 static public EnumSet<PresetType> getType(String types) throws SAXException { 1316 if (typeCache.containsKey(types)) 1317 return typeCache.get(types); 1318 EnumSet<PresetType> result = EnumSet.noneOf(PresetType.class); 1319 for (String type : Arrays.asList(types.split(","))) { 1320 try { 1321 PresetType presetType = PresetType.fromString(type); 1322 result.add(presetType); 1323 } catch (IllegalArgumentException e) { 1324 throw new SAXException(tr("Unknown type: {0}", type)); 1325 } 1326 } 1327 typeCache.put(types, result); 1328 return result; 1329 } 1330 1331 /* 1332 * Called from the XML parser to set the types this preset affects. 1333 */ 1334 public void setType(String types) throws SAXException { 1335 this.types = getType(types); 1336 } 1337 1338 public void setName_template(String pattern) throws SAXException { 1339 try { 1340 this.nameTemplate = new TemplateParser(pattern).parse(); 1341 } catch (ParseError e) { 1342 System.err.println("Error while parsing " + pattern + ": " + e.getMessage()); 1343 throw new SAXException(e); 1344 } 1345 } 1346 1347 public void setName_template_filter(String filter) throws SAXException { 1348 try { 1349 this.nameTemplateFilter = SearchCompiler.compile(filter, false, false); 1350 } catch (org.openstreetmap.josm.actions.search.SearchCompiler.ParseError e) { 1351 System.err.println("Error while parsing" + filter + ": " + e.getMessage()); 1352 throw new SAXException(e); 1353 } 1354 } 1355 1356 1357 public static List<TaggingPreset> readAll(Reader in, boolean validate) throws SAXException { 1358 XmlObjectParser parser = new XmlObjectParser(); 1359 parser.mapOnStart("item", TaggingPreset.class); 1360 parser.mapOnStart("separator", TaggingPresetSeparator.class); 1361 parser.mapBoth("group", TaggingPresetMenu.class); 1362 parser.map("text", Text.class); 1363 parser.map("link", Link.class); 1364 parser.mapOnStart("optional", Optional.class); 1365 parser.mapOnStart("roles", Roles.class); 1366 parser.map("role", Role.class); 1367 parser.map("check", Check.class); 1368 parser.map("combo", Combo.class); 1369 parser.map("multiselect", MultiSelect.class); 1370 parser.map("label", Label.class); 1371 parser.map("space", Space.class); 1372 parser.map("key", Key.class); 1373 parser.map("list_entry", PresetListEntry.class); 1374 LinkedList<TaggingPreset> all = new LinkedList<TaggingPreset>(); 1375 TaggingPresetMenu lastmenu = null; 1376 Roles lastrole = null; 1377 List<PresetListEntry> listEntries = new LinkedList<PresetListEntry>(); 1378 1379 if (validate) { 1380 parser.startWithValidation(in, "http://josm.openstreetmap.de/tagging-preset-1.0", "resource://data/tagging-preset.xsd"); 1381 } else { 1382 parser.start(in); 1383 } 1384 while(parser.hasNext()) { 1385 Object o = parser.next(); 1386 if (o instanceof TaggingPresetMenu) { 1387 TaggingPresetMenu tp = (TaggingPresetMenu) o; 1388 if(tp == lastmenu) { 1389 lastmenu = tp.group; 1390 } else 1391 { 1392 tp.group = lastmenu; 1393 tp.setDisplayName(); 1394 lastmenu = tp; 1395 all.add(tp); 1396 1397 } 1398 lastrole = null; 1399 } else if (o instanceof TaggingPresetSeparator) { 1400 TaggingPresetSeparator tp = (TaggingPresetSeparator) o; 1401 tp.group = lastmenu; 1402 all.add(tp); 1403 lastrole = null; 1404 } else if (o instanceof TaggingPreset) { 1405 TaggingPreset tp = (TaggingPreset) o; 1406 tp.group = lastmenu; 1407 tp.setDisplayName(); 1408 all.add(tp); 1409 lastrole = null; 1410 } else { 1411 if (all.size() != 0) { 1412 if (o instanceof Roles) { 1413 all.getLast().data.add((Item) o); 1414 lastrole = (Roles) o; 1415 } else if (o instanceof Role) { 1416 if (lastrole == null) 1417 throw new SAXException(tr("Preset role element without parent")); 1418 lastrole.roles.add((Role) o); 1419 } else if (o instanceof PresetListEntry) { 1420 listEntries.add((PresetListEntry) o); 1421 } else { 1422 all.getLast().data.add((Item) o); 1423 if (o instanceof ComboMultiSelect) { 1424 ((ComboMultiSelect) o).addListEntries(listEntries); 1425 } 1426 listEntries = new LinkedList<PresetListEntry>(); 1427 lastrole = null; 1428 } 1429 } else 1430 throw new SAXException(tr("Preset sub element without parent")); 1431 } 1432 } 1433 return all; 1434 } 1435 1436 public static Collection<TaggingPreset> readAll(String source, boolean validate) throws SAXException, IOException { 1437 Collection<TaggingPreset> tp; 1438 MirroredInputStream s = new MirroredInputStream(source); 1439 try { 1440 InputStream zip = s.getZipEntry("xml","preset"); 1441 if(zip != null) { 1442 zipIcons = s.getFile(); 1443 } 1444 InputStreamReader r; 1445 try { 1446 r = new InputStreamReader(zip == null ? s : zip, "UTF-8"); 1447 } catch (UnsupportedEncodingException e) { 1448 r = new InputStreamReader(zip == null ? s: zip); 1449 } 1450 try { 1451 tp = TaggingPreset.readAll(new BufferedReader(r), validate); 1452 } finally { 1453 r.close(); 1454 } 1455 } finally { 1456 s.close(); 1457 } 1458 return tp; 1459 } 1460 1461 public static Collection<TaggingPreset> readAll(Collection<String> sources, boolean validate) { 1462 LinkedList<TaggingPreset> allPresets = new LinkedList<TaggingPreset>(); 1463 for(String source : sources) { 1464 try { 1465 allPresets.addAll(TaggingPreset.readAll(source, validate)); 1466 } catch (IOException e) { 1467 e.printStackTrace(); 1468 JOptionPane.showMessageDialog( 1469 Main.parent, 1470 tr("Could not read tagging preset source: {0}",source), 1471 tr("Error"), 1472 JOptionPane.ERROR_MESSAGE 1473 ); 1474 } catch (SAXException e) { 1475 System.err.println(e.getMessage()); 1476 System.err.println(source); 1477 e.printStackTrace(); 1478 JOptionPane.showMessageDialog( 1479 Main.parent, 1480 tr("Error parsing {0}: ", source)+e.getMessage(), 1481 tr("Error"), 1482 JOptionPane.ERROR_MESSAGE 1483 ); 1484 } 1485 } 1486 return allPresets; 1487 } 1488 1489 public static LinkedList<String> getPresetSources() { 1490 LinkedList<String> sources = new LinkedList<String>(); 1491 1492 for (SourceEntry e : (new PresetPrefHelper()).get()) { 1493 sources.add(e.url); 1494 } 1495 1496 return sources; 1497 } 1498 1499 public static Collection<TaggingPreset> readFromPreferences(boolean validate) { 1500 return readAll(getPresetSources(), validate); 1501 } 1502 1503 private static class PresetPanel extends JPanel { 1504 boolean hasElements = false; 1505 PresetPanel() 1506 { 1507 super(new GridBagLayout()); 1508 } 1509 } 1510 1511 public PresetPanel createPanel(Collection<OsmPrimitive> selected) { 1512 if (data == null) 1513 return null; 1514 PresetPanel p = new PresetPanel(); 1515 LinkedList<Item> l = new LinkedList<Item>(); 1516 if(types != null){ 1517 JPanel pp = new JPanel(); 1518 for(PresetType t : types){ 1519 JLabel la = new JLabel(ImageProvider.get(t.getIconName())); 1520 la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName()))); 1521 pp.add(la); 1522 } 1523 p.add(pp, GBC.eol()); 1524 } 1525 1526 JPanel items = new JPanel(new GridBagLayout()); 1527 for (Item i : data){ 1528 if(i instanceof Link) { 1529 l.add(i); 1530 } else { 1531 if(i.addToPanel(items, selected)) { 1532 p.hasElements = true; 1533 } 1534 } 1535 } 1536 p.add(items, GBC.eol().fill()); 1537 if (selected.size() == 0 && !supportsRelation()) { 1538 GuiHelper.setEnabledRec(items, false); 1539 } 1540 1541 for(Item link : l) { 1542 link.addToPanel(p, selected); 1543 } 1544 1545 return p; 1546 } 1547 1548 public boolean isShowable() 1549 { 1550 for(Item i : data) 1551 { 1552 if(!(i instanceof Optional || i instanceof Space || i instanceof Key)) 1553 return true; 1554 } 1555 return false; 1556 } 1557 1558 public void actionPerformed(ActionEvent e) { 1559 if (Main.main == null) return; 1560 if (Main.main.getCurrentDataSet() == null) return; 1561 1562 Collection<OsmPrimitive> sel = createSelection(Main.main.getCurrentDataSet().getSelected()); 1563 int answer = showDialog(sel, supportsRelation()); 1564 1565 if (sel.size() != 0 && answer == DIALOG_ANSWER_APPLY) { 1566 Command cmd = createCommand(sel, getChangedTags()); 1567 if (cmd != null) { 1568 Main.main.undoRedo.add(cmd); 1569 } 1570 } else if (answer == DIALOG_ANSWER_NEW_RELATION) { 1571 final Relation r = new Relation(); 1572 final Collection<RelationMember> members = new HashSet<RelationMember>(); 1573 for(Tag t : getChangedTags()) { 1574 r.put(t.getKey(), t.getValue()); 1575 } 1576 for(OsmPrimitive osm : Main.main.getCurrentDataSet().getSelected()) { 1577 RelationMember rm = new RelationMember("", osm); 1578 r.addMember(rm); 1579 members.add(rm); 1580 } 1581 SwingUtilities.invokeLater(new Runnable() { 1582 @Override 1583 public void run() { 1584 RelationEditor.getEditor(Main.main.getEditLayer(), r, members).setVisible(true); 1585 } 1586 }); 1587 } 1588 Main.main.getCurrentDataSet().setSelected(Main.main.getCurrentDataSet().getSelected()); // force update 1589 1590 } 1591 1592 public int showDialog(Collection<OsmPrimitive> sel, final boolean showNewRelation) { 1593 PresetPanel p = createPanel(sel); 1594 if (p == null) 1595 return DIALOG_ANSWER_CANCEL; 1596 1597 int answer = 1; 1598 if (p.getComponentCount() != 0 && (sel.size() == 0 || p.hasElements)) { 1599 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size()); 1600 if(sel.size() == 0) { 1601 if(originalSelectionEmpty) { 1602 title = tr("Nothing selected!"); 1603 } else { 1604 title = tr("Selection unsuitable!"); 1605 } 1606 } 1607 1608 class PresetDialog extends ExtendedDialog { 1609 public PresetDialog(Component content, String title, boolean disableApply) { 1610 super(Main.parent, 1611 title, 1612 showNewRelation? 1613 new String[] { tr("Apply Preset"), tr("New relation"), tr("Cancel") }: 1614 new String[] { tr("Apply Preset"), tr("Cancel") }, 1615 true); 1616 contentInsets = new Insets(10,5,0,5); 1617 if (showNewRelation) { 1618 setButtonIcons(new String[] {"ok.png", "dialogs/addrelation.png", "cancel.png" }); 1619 } else { 1620 setButtonIcons(new String[] {"ok.png", "cancel.png" }); 1621 } 1622 setContent(content); 1623 setDefaultButton(1); 1624 setupDialog(); 1625 buttons.get(0).setEnabled(!disableApply); 1626 buttons.get(0).setToolTipText(title); 1627 // Prevent dialogs of being too narrow (fix #6261) 1628 Dimension d = getSize(); 1629 if (d.width < 350) { 1630 d.width = 350; 1631 setSize(d); 1632 } 1633 showDialog(); 1634 } 1635 } 1636 1637 answer = new PresetDialog(p, title, (sel.size() == 0)).getValue(); 1638 } 1639 if (!showNewRelation && answer == 2) 1640 return DIALOG_ANSWER_CANCEL; 1641 else 1642 return answer; 1643 } 1644 1645 /** 1646 * True whenever the original selection given into createSelection was empty 1647 */ 1648 private boolean originalSelectionEmpty = false; 1649 1650 /** 1651 * Removes all unsuitable OsmPrimitives from the given list 1652 * @param participants List of possible OsmPrimitives to tag 1653 * @return Cleaned list with suitable OsmPrimitives only 1654 */ 1655 public Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) { 1656 originalSelectionEmpty = participants.size() == 0; 1657 Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>(); 1658 for (OsmPrimitive osm : participants) 1659 { 1660 if (types != null) 1661 { 1662 if(osm instanceof Relation) 1663 { 1664 if(!types.contains(PresetType.RELATION) && 1665 !(types.contains(PresetType.CLOSEDWAY) && ((Relation)osm).isMultipolygon())) { 1666 continue; 1667 } 1668 } 1669 else if(osm instanceof Node) 1670 { 1671 if(!types.contains(PresetType.NODE)) { 1672 continue; 1673 } 1674 } 1675 else if(osm instanceof Way) 1676 { 1677 if(!types.contains(PresetType.WAY) && 1678 !(types.contains(PresetType.CLOSEDWAY) && ((Way)osm).isClosed())) { 1679 continue; 1680 } 1681 } 1682 } 1683 sel.add(osm); 1684 } 1685 return sel; 1686 } 1687 1688 public List<Tag> getChangedTags() { 1689 List<Tag> result = new ArrayList<Tag>(); 1690 for (Item i: data) { 1691 i.addCommands(result); 1692 } 1693 return result; 1694 } 1695 1696 private static String fixPresetString(String s) { 1697 return s == null ? s : s.replaceAll("'","''"); 1698 } 1699 1700 public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) { 1701 List<Command> cmds = new ArrayList<Command>(); 1702 for (Tag tag: changedTags) { 1703 cmds.add(new ChangePropertyCommand(sel, tag.getKey(), tag.getValue())); 1704 } 1705 1706 if (cmds.size() == 0) 1707 return null; 1708 else if (cmds.size() == 1) 1709 return cmds.get(0); 1710 else 1711 return new SequenceCommand(tr("Change Properties"), cmds); 1712 } 1713 1714 private boolean supportsRelation() { 1715 return types == null || types.contains(PresetType.RELATION); 1716 } 1717 1718 protected void updateEnabledState() { 1719 setEnabled(Main.main != null && Main.main.getCurrentDataSet() != null); 1720 } 1721 1722 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 1723 updateEnabledState(); 1724 } 1725 1726 public void layerAdded(Layer newLayer) { 1727 updateEnabledState(); 1728 } 1729 1730 public void layerRemoved(Layer oldLayer) { 1731 updateEnabledState(); 1732 } 1733 1734 @Override 1735 public String toString() { 1736 return (types == null?"":types) + " " + name; 1737 } 1738 1739 public boolean typeMatches(Collection<PresetType> t) { 1740 return t == null || types == null || types.containsAll(t); 1741 } 1742 1743 public boolean matches(Collection<PresetType> t, Map<String, String> tags, boolean onlyShowable) { 1744 if (onlyShowable && !isShowable()) 1745 return false; 1746 else if (!typeMatches(t)) 1747 return false; 1748 boolean atLeastOnePositiveMatch = false; 1749 for (Item item : data) { 1750 Boolean m = item.matches(tags); 1751 if (m != null && !m) 1752 return false; 1753 else if (m != null) { 1754 atLeastOnePositiveMatch = true; 1755 } 1756 } 1757 return atLeastOnePositiveMatch; 1758 } 1759 }