001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.presets; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.Component; 009import java.awt.ComponentOrientation; 010import java.awt.Dimension; 011import java.awt.GridBagLayout; 012import java.awt.Insets; 013import java.awt.event.ActionEvent; 014import java.io.File; 015import java.util.ArrayList; 016import java.util.Collection; 017import java.util.EnumSet; 018import java.util.LinkedHashSet; 019import java.util.List; 020import java.util.Map; 021import java.util.Objects; 022import java.util.Set; 023import java.util.concurrent.CompletableFuture; 024import java.util.function.Predicate; 025import java.util.stream.Collectors; 026 027import javax.swing.AbstractAction; 028import javax.swing.Action; 029import javax.swing.ImageIcon; 030import javax.swing.JLabel; 031import javax.swing.JOptionPane; 032import javax.swing.JPanel; 033import javax.swing.JToggleButton; 034import javax.swing.SwingUtilities; 035 036import org.openstreetmap.josm.actions.AdaptableAction; 037import org.openstreetmap.josm.actions.CreateMultipolygonAction; 038import org.openstreetmap.josm.command.ChangePropertyCommand; 039import org.openstreetmap.josm.command.Command; 040import org.openstreetmap.josm.command.SequenceCommand; 041import org.openstreetmap.josm.data.UndoRedoHandler; 042import org.openstreetmap.josm.data.osm.DataSet; 043import org.openstreetmap.josm.data.osm.IPrimitive; 044import org.openstreetmap.josm.data.osm.OsmData; 045import org.openstreetmap.josm.data.osm.OsmDataManager; 046import org.openstreetmap.josm.data.osm.OsmPrimitive; 047import org.openstreetmap.josm.data.osm.Relation; 048import org.openstreetmap.josm.data.osm.RelationMember; 049import org.openstreetmap.josm.data.osm.Tag; 050import org.openstreetmap.josm.data.osm.Tagged; 051import org.openstreetmap.josm.data.osm.Way; 052import org.openstreetmap.josm.data.osm.search.SearchCompiler; 053import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 054import org.openstreetmap.josm.data.osm.search.SearchParseError; 055import org.openstreetmap.josm.data.preferences.BooleanProperty; 056import org.openstreetmap.josm.gui.ExtendedDialog; 057import org.openstreetmap.josm.gui.MainApplication; 058import org.openstreetmap.josm.gui.Notification; 059import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; 060import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter; 061import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 062import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 063import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 064import org.openstreetmap.josm.gui.tagging.presets.items.Key; 065import org.openstreetmap.josm.gui.tagging.presets.items.Link; 066import org.openstreetmap.josm.gui.tagging.presets.items.Optional; 067import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink; 068import org.openstreetmap.josm.gui.tagging.presets.items.Roles; 069import org.openstreetmap.josm.gui.tagging.presets.items.Space; 070import org.openstreetmap.josm.gui.util.GuiHelper; 071import org.openstreetmap.josm.tools.GBC; 072import org.openstreetmap.josm.tools.ImageProvider; 073import org.openstreetmap.josm.tools.ImageResource; 074import org.openstreetmap.josm.tools.Logging; 075import org.openstreetmap.josm.tools.Pair; 076import org.openstreetmap.josm.tools.StreamUtils; 077import org.openstreetmap.josm.tools.Utils; 078import org.openstreetmap.josm.tools.template_engine.ParseError; 079import org.openstreetmap.josm.tools.template_engine.TemplateEntry; 080import org.openstreetmap.josm.tools.template_engine.TemplateParser; 081import org.xml.sax.SAXException; 082 083/** 084 * This class read encapsulate one tagging preset. A class method can 085 * read in all predefined presets, either shipped with JOSM or that are 086 * in the config directory. 087 * 088 * It is also able to construct dialogs out of preset definitions. 089 * @since 294 090 */ 091public class TaggingPreset extends AbstractAction implements ActiveLayerChangeListener, AdaptableAction, Predicate<IPrimitive> { 092 093 /** The user pressed the "Apply" button */ 094 public static final int DIALOG_ANSWER_APPLY = 1; 095 /** The user pressed the "New Relation" button */ 096 public static final int DIALOG_ANSWER_NEW_RELATION = 2; 097 /** The user pressed the "Cancel" button */ 098 public static final int DIALOG_ANSWER_CANCEL = 3; 099 100 /** The action key for optional tooltips */ 101 public static final String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text"; 102 103 /** Prefix of preset icon loading failure error message */ 104 public static final String PRESET_ICON_ERROR_MSG_PREFIX = "Could not get presets icon "; 105 106 /** 107 * Defines whether the validator should be active in the preset dialog 108 * @see TaggingPresetValidation 109 */ 110 public static final BooleanProperty USE_VALIDATOR = new BooleanProperty("taggingpreset.validator", false); 111 112 /** 113 * The preset group this preset belongs to. 114 */ 115 public TaggingPresetMenu group; 116 117 /** 118 * The name of the tagging preset. 119 * @see #getRawName() 120 */ 121 public String name; 122 /** 123 * The icon name assigned to this preset. 124 */ 125 public String iconName; 126 /** 127 * Translation context for name 128 */ 129 public String name_context; 130 /** 131 * A cache for the local name. Should never be accessed directly. 132 * @see #getLocaleName() 133 */ 134 public String locale_name; 135 /** 136 * Show the preset name if true 137 */ 138 public boolean preset_name_label; 139 140 /** 141 * The types as preparsed collection. 142 */ 143 public transient Set<TaggingPresetType> types; 144 /** 145 * The list of preset items 146 */ 147 public final transient List<TaggingPresetItem> data = new ArrayList<>(2); 148 /** 149 * The roles for this relation (if we are editing a relation). See: 150 * <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Tags">JOSM wiki</a> 151 */ 152 public transient Roles roles; 153 /** 154 * The name_template custom name formatter. See: 155 * <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Attributes">JOSM wiki</a> 156 */ 157 public transient TemplateEntry nameTemplate; 158 /** The name_template_filter */ 159 public transient Match nameTemplateFilter; 160 /** The match_expression */ 161 public transient Match matchExpression; 162 163 /** 164 * True whenever the original selection given into createSelection was empty 165 */ 166 private boolean originalSelectionEmpty; 167 168 /** The completable future task of asynchronous icon loading */ 169 private CompletableFuture<Void> iconFuture; 170 171 /** Support functions */ 172 protected TaggingPresetItemGuiSupport itemGuiSupport; 173 174 /** 175 * Create an empty tagging preset. This will not have any items and 176 * will be an empty string as text. createPanel will return null. 177 * Use this as default item for "do not select anything". 178 */ 179 public TaggingPreset() { 180 updateEnabledState(); 181 } 182 183 /** 184 * Change the display name without changing the toolbar value. 185 */ 186 public void setDisplayName() { 187 putValue(Action.NAME, getName()); 188 putValue("toolbar", "tagging_" + getRawName()); 189 putValue(OPTIONAL_TOOLTIP_TEXT, group != null ? 190 tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) : 191 tr("Use preset ''{0}''", getLocaleName())); 192 } 193 194 /** 195 * Gets the localized version of the name 196 * @return The name that should be displayed to the user. 197 */ 198 public String getLocaleName() { 199 if (locale_name == null) { 200 if (name_context != null) { 201 locale_name = trc(name_context, TaggingPresetItem.fixPresetString(name)); 202 } else { 203 locale_name = tr(TaggingPresetItem.fixPresetString(name)); 204 } 205 } 206 return locale_name; 207 } 208 209 /** 210 * Returns the translated name of this preset, prefixed with the group names it belongs to. 211 * @return the translated name of this preset, prefixed with the group names it belongs to 212 */ 213 public String getName() { 214 return group != null ? group.getName() + '/' + getLocaleName() : getLocaleName(); 215 } 216 217 /** 218 * Returns the non translated name of this preset, prefixed with the (non translated) group names it belongs to. 219 * @return the non translated name of this preset, prefixed with the (non translated) group names it belongs to 220 */ 221 public String getRawName() { 222 return group != null ? group.getRawName() + '/' + name : name; 223 } 224 225 /** 226 * Returns the preset icon (16px). 227 * @return The preset icon, or {@code null} if none defined 228 * @since 6403 229 */ 230 public final ImageIcon getIcon() { 231 return getIcon(Action.SMALL_ICON); 232 } 233 234 /** 235 * Returns the preset icon (16 or 24px). 236 * @param key Key determining icon size: {@code Action.SMALL_ICON} for 16x, {@code Action.LARGE_ICON_KEY} for 24px 237 * @return The preset icon, or {@code null} if none defined 238 * @since 10849 239 */ 240 public final ImageIcon getIcon(String key) { 241 Object icon = getValue(key); 242 if (icon instanceof ImageIcon) { 243 return (ImageIcon) icon; 244 } 245 return null; 246 } 247 248 /** 249 * Returns the {@link ImageResource} attached to this preset, if any. 250 * @return the {@code ImageResource} attached to this preset, or {@code null} 251 * @since 16060 252 */ 253 public final ImageResource getImageResource() { 254 return ImageResource.getAttachedImageResource(this); 255 } 256 257 /** 258 * Called from the XML parser to set the icon. 259 * The loading task is performed in the background in order to speedup startup. 260 * @param iconName icon name 261 */ 262 public void setIcon(final String iconName) { 263 this.iconName = iconName; 264 if (iconName == null || !TaggingPresetReader.isLoadIcons()) { 265 return; 266 } 267 File arch = TaggingPresetReader.getZipIcons(); 268 final Collection<String> s = TaggingPresets.ICON_SOURCES.get(); 269 this.iconFuture = new CompletableFuture<>(); 270 new ImageProvider(iconName) 271 .setDirs(s) 272 .setId("presets") 273 .setArchive(arch) 274 .setOptional(true) 275 .getResourceAsync(result -> { 276 if (result != null) { 277 GuiHelper.runInEDT(() -> { 278 try { 279 result.attachImageIcon(this, true); 280 } catch (IllegalArgumentException e) { 281 Logging.warn(toString() + ": " + PRESET_ICON_ERROR_MSG_PREFIX + iconName); 282 Logging.warn(e); 283 } finally { 284 iconFuture.complete(null); 285 } 286 }); 287 } else { 288 Logging.warn(toString() + ": " + PRESET_ICON_ERROR_MSG_PREFIX + iconName); 289 iconFuture.complete(null); 290 } 291 }); 292 } 293 294 /** 295 * Called from the XML parser to set the types this preset affects. 296 * @param types comma-separated primitive types ("node", "way", "relation" or "closedway") 297 * @throws SAXException if any SAX error occurs 298 * @see TaggingPresetType#fromString 299 */ 300 public void setType(String types) throws SAXException { 301 this.types = TaggingPresetItem.getType(types); 302 } 303 304 /** 305 * Sets the name_template custom name formatter. 306 * 307 * @param template The format template 308 * @throws SAXException on template parse error 309 * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#name_templatedetails">JOSM wiki</a> 310 */ 311 public void setName_template(String template) throws SAXException { 312 try { 313 this.nameTemplate = new TemplateParser(template).parse(); 314 } catch (ParseError e) { 315 Logging.error("Error while parsing " + template + ": " + e.getMessage()); 316 throw new SAXException(e); 317 } 318 } 319 320 /** 321 * Sets the name_template_filter. 322 * 323 * @param filter The search pattern 324 * @throws SAXException on search patern parse error 325 * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#name_templatedetails">JOSM wiki</a> 326 */ 327 public void setName_template_filter(String filter) throws SAXException { 328 try { 329 this.nameTemplateFilter = SearchCompiler.compile(filter); 330 } catch (SearchParseError e) { 331 Logging.error("Error while parsing" + filter + ": " + e.getMessage()); 332 throw new SAXException(e); 333 } 334 } 335 336 /** 337 * Sets the match_expression additional criteria for matching primitives. 338 * 339 * @param filter The search pattern 340 * @throws SAXException on search patern parse error 341 * @see <a href="https://josm.openstreetmap.de/wiki/TaggingPresets#Attributes">JOSM wiki</a> 342 */ 343 public void setMatch_expression(String filter) throws SAXException { 344 try { 345 this.matchExpression = SearchCompiler.compile(filter); 346 } catch (SearchParseError e) { 347 Logging.error("Error while parsing" + filter + ": " + e.getMessage()); 348 throw new SAXException(e); 349 } 350 } 351 352 private static class PresetPanel extends JPanel { 353 private boolean hasElements; 354 355 PresetPanel() { 356 super(new GridBagLayout()); 357 } 358 } 359 360 /** 361 * Creates a panel for this preset. This includes general information such as name and supported {@link TaggingPresetType types}. 362 * This includes the elements from the individual {@link TaggingPresetItem items}. 363 * 364 * @param selected the selected primitives 365 * @return the newly created panel 366 */ 367 public PresetPanel createPanel(Collection<OsmPrimitive> selected) { 368 PresetPanel p = new PresetPanel(); 369 370 final JPanel pp = new JPanel(); 371 if (types != null) { 372 for (TaggingPresetType t : types) { 373 JLabel la = new JLabel(ImageProvider.get(t.getIconName())); 374 la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName()))); 375 pp.add(la); 376 } 377 } 378 final List<Tag> directlyAppliedTags = Utils.filteredCollection(data, Key.class).stream() 379 .map(Key::asTag) 380 .collect(Collectors.toList()); 381 if (!directlyAppliedTags.isEmpty()) { 382 final JLabel label = new JLabel(ImageProvider.get("pastetags")); 383 label.setToolTipText("<html>" + tr("This preset also sets: {0}", Utils.joinAsHtmlUnorderedList(directlyAppliedTags))); 384 pp.add(label); 385 } 386 JLabel validationLabel = new JLabel(ImageProvider.get("warning-small", ImageProvider.ImageSizes.LARGEICON)); 387 validationLabel.setVisible(false); 388 pp.add(validationLabel); 389 390 final int count = pp.getComponentCount(); 391 if (preset_name_label) { 392 p.add(new JLabel(getIcon(Action.LARGE_ICON_KEY)), GBC.std(0, 0).span(1, count > 0 ? 2 : 1).insets(0, 0, 5, 0)); 393 } 394 if (count > 0) { 395 p.add(pp, GBC.std(1, 0).span(GBC.REMAINDER)); 396 } 397 if (preset_name_label) { 398 p.add(new JLabel(getName()), GBC.std(1, count > 0 ? 1 : 0).insets(5, 0, 0, 0).span(GBC.REMAINDER).fill(GBC.HORIZONTAL)); 399 } 400 401 boolean presetInitiallyMatches = !selected.isEmpty() && selected.stream().allMatch(this); 402 itemGuiSupport = TaggingPresetItemGuiSupport.create(presetInitiallyMatches, selected, this::getChangedTags); 403 404 JPanel itemPanel = new JPanel(new GridBagLayout()) { 405 /** 406 * This hack allows the items to have their own orientation. 407 * 408 * The problem is that 409 * {@link org.openstreetmap.josm.gui.ExtendedDialog#showDialog ExtendedDialog} calls 410 * {@code applyComponentOrientation} very late in the dialog construction process thus 411 * overwriting the orientation the components have chosen for themselves. 412 * 413 * This stops the propagation of {@code applyComponentOrientation}, thus all 414 * {@code TaggingPresetItem}s may (and have to) set their own orientation. 415 */ 416 @Override 417 public void applyComponentOrientation(ComponentOrientation o) { 418 setComponentOrientation(o); 419 } 420 }; 421 JPanel linkPanel = new JPanel(new GridBagLayout()); 422 TaggingPresetItem previous = null; 423 for (TaggingPresetItem i : data) { 424 if (i instanceof Link) { 425 i.addToPanel(linkPanel, itemGuiSupport); 426 p.hasElements = true; 427 } else { 428 if (i instanceof PresetLink) { 429 PresetLink link = (PresetLink) i; 430 if (!(previous instanceof PresetLink && Objects.equals(((PresetLink) previous).text, link.text))) { 431 itemPanel.add(link.createLabel(), GBC.eol().insets(0, 8, 0, 0)); 432 } 433 } 434 if (i.addToPanel(itemPanel, itemGuiSupport)) { 435 p.hasElements = true; 436 } 437 } 438 previous = i; 439 } 440 p.add(itemPanel, GBC.eol().fill()); 441 p.add(linkPanel, GBC.eol().fill()); 442 443 if (selected.isEmpty() && !supportsRelation()) { 444 GuiHelper.setEnabledRec(itemPanel, false); 445 } 446 447 if (selected.size() == 1 && USE_VALIDATOR.get()) { 448 itemGuiSupport.addListener((source, key, newValue) -> 449 TaggingPresetValidation.validateAsync(selected.iterator().next(), validationLabel, getChangedTags())); 450 } 451 452 // "Add toolbar button" 453 JToggleButton tb = new JToggleButton(new ToolbarButtonAction()); 454 tb.setFocusable(false); 455 p.add(tb, GBC.std(1, 0).anchor(GBC.LINE_END)); 456 457 // Trigger initial updates once and only once 458 itemGuiSupport.setEnabled(true); 459 itemGuiSupport.fireItemValueModified(null, null, null); 460 461 return p; 462 } 463 464 /** 465 * Determines whether a dialog can be shown for this preset, i.e., at least one tag can/must be set by the user. 466 * 467 * @return {@code true} if a dialog can be shown for this preset 468 */ 469 public boolean isShowable() { 470 return data.stream().anyMatch(i -> !(i instanceof Optional || i instanceof Space || i instanceof Key)); 471 } 472 473 /** 474 * Suggests a relation role for this primitive 475 * 476 * @param osm The primitive 477 * @return the suggested role or null 478 */ 479 public String suggestRoleForOsmPrimitive(OsmPrimitive osm) { 480 if (roles != null && osm != null) { 481 return roles.roles.stream() 482 .filter(i -> i.memberExpression != null && i.memberExpression.match(osm)) 483 .filter(i -> Utils.isEmpty(i.types) || i.types.contains(TaggingPresetType.forPrimitive(osm))) 484 .findFirst() 485 .map(i -> i.key) 486 .orElse(null); 487 } 488 return null; 489 } 490 491 @Override 492 public void actionPerformed(ActionEvent e) { 493 DataSet ds = OsmDataManager.getInstance().getEditDataSet(); 494 if (ds == null) { 495 return; 496 } 497 showAndApply(ds.getSelected()); 498 } 499 500 /** 501 * {@linkplain #showDialog Show preset dialog}, apply changes 502 * @param primitives the primitives 503 */ 504 public void showAndApply(Collection<OsmPrimitive> primitives) { 505 // Display dialog even if no data layer (used by preset-tagging-tester plugin) 506 Collection<OsmPrimitive> sel = createSelection(primitives); 507 int answer = showDialog(sel, supportsRelation()); 508 509 if (!sel.isEmpty() && answer == DIALOG_ANSWER_APPLY) { 510 Command cmd = createCommand(sel, getChangedTags()); 511 if (cmd != null) { 512 UndoRedoHandler.getInstance().add(cmd); 513 } 514 } else if (answer == DIALOG_ANSWER_NEW_RELATION) { 515 Relation calculated = null; 516 if (getChangedTags().stream().anyMatch(t -> "boundary".equals(t.get("type")) || "multipolygon".equals(t.get("type")))) { 517 Collection<Way> ways = Utils.filteredCollection(primitives, Way.class); 518 Pair<Relation, Relation> res = CreateMultipolygonAction.createMultipolygonRelation(ways, true); 519 if (res != null) { 520 calculated = res.b; 521 } 522 } 523 final Relation r = calculated != null ? calculated : new Relation(); 524 final Collection<RelationMember> members = new LinkedHashSet<>(r.getMembers()); 525 for (Tag t : getChangedTags()) { 526 r.put(t.getKey(), t.getValue()); 527 } 528 for (OsmPrimitive osm : primitives) { 529 if (r == calculated && osm instanceof Way) 530 continue; 531 String role = suggestRoleForOsmPrimitive(osm); 532 RelationMember rm = new RelationMember(role == null ? "" : role, osm); 533 r.addMember(rm); 534 members.add(rm); 535 } 536 if (r.isMultipolygon() && r != calculated) { 537 r.setMembers(RelationSorter.sortMembersByConnectivity(r.getMembers())); 538 } 539 SwingUtilities.invokeLater(() -> RelationEditor.getEditor( 540 MainApplication.getLayerManager().getEditLayer(), r, members).setVisible(true)); 541 } 542 if (!primitives.isEmpty()) { 543 DataSet ds = primitives.iterator().next().getDataSet(); 544 ds.setSelected(primitives); // force update 545 } 546 } 547 548 private static class PresetDialog extends ExtendedDialog { 549 550 /** 551 * Constructs a new {@code PresetDialog}. 552 * @param content the content that will be displayed in this dialog 553 * @param title the text that will be shown in the window titlebar 554 * @param icon the image to be displayed as the icon for this window 555 * @param disableApply whether to disable "Apply" button 556 * @param showNewRelation whether to display "New relation" button 557 */ 558 PresetDialog(Component content, String title, ImageIcon icon, boolean disableApply, boolean showNewRelation) { 559 super(MainApplication.getMainFrame(), title, 560 showNewRelation ? 561 (new String[] {tr("Apply Preset"), tr("New relation"), tr("Cancel")}) : 562 (new String[] {tr("Apply Preset"), tr("Cancel")}), 563 true); 564 if (icon != null) 565 setIconImage(icon.getImage()); 566 contentInsets = new Insets(10, 5, 0, 5); 567 if (showNewRelation) { 568 setButtonIcons("ok", "data/relation", "cancel"); 569 } else { 570 setButtonIcons("ok", "cancel"); 571 } 572 configureContextsensitiveHelp("/Menu/Presets", true); 573 setContent(content); 574 setDefaultButton(1); 575 setupDialog(); 576 buttons.get(0).setEnabled(!disableApply); 577 buttons.get(0).setToolTipText(title); 578 // Prevent dialogs of being too narrow (fix #6261) 579 Dimension d = getSize(); 580 if (d.width < 350) { 581 d.width = 350; 582 setSize(d); 583 } 584 super.showDialog(); 585 } 586 } 587 588 /** 589 * Shows the preset dialog. 590 * @param sel selection 591 * @param showNewRelation whether to display "New relation" button 592 * @return the user choice after the dialog has been closed 593 */ 594 public int showDialog(Collection<OsmPrimitive> sel, boolean showNewRelation) { 595 PresetPanel p = createPanel(sel); 596 597 int answer = 1; 598 boolean canCreateRelation = types == null || types.contains(TaggingPresetType.RELATION); 599 if (originalSelectionEmpty && !canCreateRelation) { 600 new Notification( 601 tr("The preset <i>{0}</i> cannot be applied since nothing has been selected!", getLocaleName())) 602 .setIcon(JOptionPane.WARNING_MESSAGE) 603 .show(); 604 return DIALOG_ANSWER_CANCEL; 605 } else if (sel.isEmpty() && !canCreateRelation) { 606 new Notification( 607 tr("The preset <i>{0}</i> cannot be applied since the selection is unsuitable!", getLocaleName())) 608 .setIcon(JOptionPane.WARNING_MESSAGE) 609 .show(); 610 return DIALOG_ANSWER_CANCEL; 611 } else if (p.getComponentCount() != 0 && (sel.isEmpty() || p.hasElements)) { 612 int size = sel.size(); 613 String title = trn("Change {0} object", "Change {0} objects", size, size); 614 if (!showNewRelation && size == 0) { 615 if (originalSelectionEmpty) { 616 title = tr("Nothing selected!"); 617 } else { 618 title = tr("Selection unsuitable!"); 619 } 620 } 621 622 boolean disableApply = size == 0; 623 if (!disableApply) { 624 OsmData<?, ?, ?, ?> ds = sel.iterator().next().getDataSet(); 625 disableApply = ds != null && ds.isLocked(); 626 } 627 answer = new PresetDialog(p, title, preset_name_label ? null : (ImageIcon) getValue(Action.SMALL_ICON), 628 disableApply, showNewRelation).getValue(); 629 } 630 if (!showNewRelation && answer == 2) 631 return DIALOG_ANSWER_CANCEL; 632 else 633 return answer; 634 } 635 636 /** 637 * Removes all unsuitable OsmPrimitives from the given list 638 * @param participants List of possible OsmPrimitives to tag 639 * @return Cleaned list with suitable OsmPrimitives only 640 */ 641 public Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) { 642 originalSelectionEmpty = participants.isEmpty(); 643 return participants.stream().filter(this::typeMatches).collect(Collectors.toList()); 644 } 645 646 /** 647 * Gets a list of tags that are set by this preset. 648 * @return The list of tags. 649 */ 650 public List<Tag> getChangedTags() { 651 List<Tag> result = new ArrayList<>(); 652 data.forEach(i -> i.addCommands(result)); 653 return result; 654 } 655 656 /** 657 * Create a command to change the given list of tags. 658 * @param sel The primitives to change the tags for 659 * @param changedTags The tags to change 660 * @return A command that changes the tags. 661 */ 662 public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) { 663 List<Command> cmds = changedTags.stream() 664 .map(tag -> new ChangePropertyCommand(sel, tag.getKey(), tag.getValue())) 665 .filter(cmd -> cmd.getObjectsNumber() > 0) 666 .collect(StreamUtils.toUnmodifiableList()); 667 return cmds.isEmpty() ? null : SequenceCommand.wrapIfNeeded(tr("Change Tags"), cmds); 668 } 669 670 private boolean supportsRelation() { 671 return types == null || types.contains(TaggingPresetType.RELATION); 672 } 673 674 protected final void updateEnabledState() { 675 setEnabled(OsmDataManager.getInstance().getEditDataSet() != null); 676 } 677 678 @Override 679 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 680 updateEnabledState(); 681 } 682 683 @Override 684 public String toString() { 685 return (types == null ? "" : types.toString()) + ' ' + name; 686 } 687 688 /** 689 * Determines whether this preset matches the OSM primitive type. 690 * @param primitive The OSM primitive for which type must match 691 * @return <code>true</code> if type matches. 692 * @since 15640 693 */ 694 public final boolean typeMatches(IPrimitive primitive) { 695 return typeMatches(EnumSet.of(TaggingPresetType.forPrimitive(primitive))); 696 } 697 698 /** 699 * Determines whether this preset matches the types. 700 * @param t The types that must match 701 * @return <code>true</code> if all types match. 702 */ 703 public boolean typeMatches(Collection<TaggingPresetType> t) { 704 return t == null || types == null || types.containsAll(t); 705 } 706 707 /** 708 * Determines whether this preset matches the given primitive, i.e., 709 * whether the {@link #typeMatches(Collection) type matches} and the {@link TaggingPresetItem#matches(Map) tags match}. 710 * 711 * @param p the primitive 712 * @return {@code true} if this preset matches the primitive 713 * @since 13623 (signature) 714 */ 715 @Override 716 public boolean test(IPrimitive p) { 717 return matches(EnumSet.of(TaggingPresetType.forPrimitive(p)), p.getKeys(), false); 718 } 719 720 /** 721 * Determines whether this preset matches the parameters. 722 * 723 * @param t the preset types to include, see {@link #typeMatches(Collection)} 724 * @param tags the tags to perform matching on, see {@link TaggingPresetItem#matches(Map)} 725 * @param onlyShowable whether the preset must be {@link #isShowable() showable} 726 * @return {@code true} if this preset matches the parameters. 727 */ 728 public boolean matches(Collection<TaggingPresetType> t, Map<String, String> tags, boolean onlyShowable) { 729 if ((onlyShowable && !isShowable()) || !typeMatches(t)) { 730 return false; 731 } else if (matchExpression != null && !matchExpression.match(Tagged.ofMap(tags))) { 732 return false; 733 } else { 734 return TaggingPresetItem.matches(data, tags); 735 } 736 } 737 738 /** 739 * Action that adds or removes the button on main toolbar 740 */ 741 public class ToolbarButtonAction extends AbstractAction { 742 private final int toolbarIndex; 743 744 /** 745 * Constructs a new {@code ToolbarButtonAction}. 746 */ 747 public ToolbarButtonAction() { 748 super(""); 749 new ImageProvider("dialogs", "pin").getResource().attachImageIcon(this, true); 750 putValue(SHORT_DESCRIPTION, tr("Add or remove toolbar button")); 751 List<String> t = new ArrayList<>(ToolbarPreferences.getToolString()); 752 toolbarIndex = t.indexOf(getToolbarString()); 753 putValue(SELECTED_KEY, toolbarIndex >= 0); 754 } 755 756 @Override 757 public void actionPerformed(ActionEvent ae) { 758 String res = getToolbarString(); 759 MainApplication.getToolbar().addCustomButton(res, toolbarIndex, true); 760 } 761 } 762 763 /** 764 * Gets a string describing this preset that can be used for the toolbar 765 * @return A String that can be passed on to the toolbar 766 * @see ToolbarPreferences#addCustomButton(String, int, boolean) 767 */ 768 public String getToolbarString() { 769 ToolbarPreferences.ActionParser actionParser = new ToolbarPreferences.ActionParser(null); 770 return actionParser.saveAction(new ToolbarPreferences.ActionDefinition(this)); 771 } 772 773 /** 774 * Returns the completable future task that performs icon loading, if any. 775 * @return the completable future task that performs icon loading, or null 776 * @since 14449 777 */ 778 public CompletableFuture<Void> getIconLoadingTask() { 779 return iconFuture; 780 } 781 782}