source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java@ 12620

Last change on this file since 12620 was 12620, checked in by Don-vip, 7 years ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • Property svn:eol-style set to native
File size: 23.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging.presets;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.GridBagLayout;
11import java.awt.Insets;
12import java.awt.event.ActionEvent;
13import java.io.File;
14import java.util.ArrayList;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.EnumSet;
18import java.util.HashSet;
19import java.util.LinkedList;
20import java.util.List;
21import java.util.Map;
22import java.util.Set;
23import java.util.function.Predicate;
24
25import javax.swing.AbstractAction;
26import javax.swing.Action;
27import javax.swing.ImageIcon;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JToggleButton;
32import javax.swing.SwingUtilities;
33
34import org.openstreetmap.josm.Main;
35import org.openstreetmap.josm.actions.AdaptableAction;
36import org.openstreetmap.josm.actions.search.SearchCompiler;
37import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
38import org.openstreetmap.josm.command.ChangePropertyCommand;
39import org.openstreetmap.josm.command.Command;
40import org.openstreetmap.josm.command.SequenceCommand;
41import org.openstreetmap.josm.data.osm.DataSet;
42import org.openstreetmap.josm.data.osm.OsmPrimitive;
43import org.openstreetmap.josm.data.osm.Relation;
44import org.openstreetmap.josm.data.osm.RelationMember;
45import org.openstreetmap.josm.data.osm.Tag;
46import org.openstreetmap.josm.gui.ExtendedDialog;
47import org.openstreetmap.josm.gui.Notification;
48import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
49import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
50import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
51import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
52import org.openstreetmap.josm.gui.tagging.presets.items.Key;
53import org.openstreetmap.josm.gui.tagging.presets.items.Link;
54import org.openstreetmap.josm.gui.tagging.presets.items.Optional;
55import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink;
56import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
57import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
58import org.openstreetmap.josm.gui.tagging.presets.items.Space;
59import org.openstreetmap.josm.gui.util.GuiHelper;
60import org.openstreetmap.josm.tools.GBC;
61import org.openstreetmap.josm.tools.ImageProvider;
62import org.openstreetmap.josm.tools.Logging;
63import org.openstreetmap.josm.tools.Utils;
64import org.openstreetmap.josm.tools.template_engine.ParseError;
65import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
66import org.openstreetmap.josm.tools.template_engine.TemplateParser;
67import org.xml.sax.SAXException;
68
69/**
70 * This class read encapsulate one tagging preset. A class method can
71 * read in all predefined presets, either shipped with JOSM or that are
72 * in the config directory.
73 *
74 * It is also able to construct dialogs out of preset definitions.
75 * @since 294
76 */
77public class TaggingPreset extends AbstractAction implements ActiveLayerChangeListener, AdaptableAction, Predicate<OsmPrimitive> {
78
79 public static final int DIALOG_ANSWER_APPLY = 1;
80 public static final int DIALOG_ANSWER_NEW_RELATION = 2;
81 public static final int DIALOG_ANSWER_CANCEL = 3;
82
83 public static final String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text";
84
85 /** Prefix of preset icon loading failure error message */
86 public static final String PRESET_ICON_ERROR_MSG_PREFIX = "Could not get presets icon ";
87
88 /**
89 * The preset group this preset belongs to.
90 */
91 public TaggingPresetMenu group;
92
93 /**
94 * The name of the tagging preset.
95 * @see #getRawName()
96 */
97 public String name;
98 /**
99 * The icon name assigned to this preset.
100 */
101 public String iconName;
102 public String name_context;
103 /**
104 * A cache for the local name. Should never be accessed directly.
105 * @see #getLocaleName()
106 */
107 public String locale_name;
108 public boolean preset_name_label;
109
110 /**
111 * The types as preparsed collection.
112 */
113 public transient Set<TaggingPresetType> types;
114 public final transient List<TaggingPresetItem> data = new LinkedList<>();
115 public transient Roles roles;
116 public transient TemplateEntry nameTemplate;
117 public transient Match nameTemplateFilter;
118
119 /**
120 * True whenever the original selection given into createSelection was empty
121 */
122 private boolean originalSelectionEmpty;
123
124 /**
125 * Create an empty tagging preset. This will not have any items and
126 * will be an empty string as text. createPanel will return null.
127 * Use this as default item for "do not select anything".
128 */
129 public TaggingPreset() {
130 Main.getLayerManager().addActiveLayerChangeListener(this);
131 updateEnabledState();
132 }
133
134 /**
135 * Change the display name without changing the toolbar value.
136 */
137 public void setDisplayName() {
138 putValue(Action.NAME, getName());
139 putValue("toolbar", "tagging_" + getRawName());
140 putValue(OPTIONAL_TOOLTIP_TEXT, group != null ?
141 tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) :
142 tr("Use preset ''{0}''", getLocaleName()));
143 }
144
145 /**
146 * Gets the localized version of the name
147 * @return The name that should be displayed to the user.
148 */
149 public String getLocaleName() {
150 if (locale_name == null) {
151 if (name_context != null) {
152 locale_name = trc(name_context, TaggingPresetItem.fixPresetString(name));
153 } else {
154 locale_name = tr(TaggingPresetItem.fixPresetString(name));
155 }
156 }
157 return locale_name;
158 }
159
160 /**
161 * Returns the translated name of this preset, prefixed with the group names it belongs to.
162 * @return the translated name of this preset, prefixed with the group names it belongs to
163 */
164 public String getName() {
165 return group != null ? group.getName() + '/' + getLocaleName() : getLocaleName();
166 }
167
168 /**
169 * Returns the non translated name of this preset, prefixed with the (non translated) group names it belongs to.
170 * @return the non translated name of this preset, prefixed with the (non translated) group names it belongs to
171 */
172 public String getRawName() {
173 return group != null ? group.getRawName() + '/' + name : name;
174 }
175
176 /**
177 * Returns the preset icon (16px).
178 * @return The preset icon, or {@code null} if none defined
179 * @since 6403
180 */
181 public final ImageIcon getIcon() {
182 return getIcon(Action.SMALL_ICON);
183 }
184
185 /**
186 * Returns the preset icon (16 or 24px).
187 * @param key Key determining icon size: {@code Action.SMALL_ICON} for 16x, {@code Action.LARGE_ICON_KEY} for 24px
188 * @return The preset icon, or {@code null} if none defined
189 * @since 10849
190 */
191 public final ImageIcon getIcon(String key) {
192 Object icon = getValue(key);
193 if (icon instanceof ImageIcon) {
194 return (ImageIcon) icon;
195 }
196 return null;
197 }
198
199 /**
200 * Called from the XML parser to set the icon.
201 * The loading task is performed in the background in order to speedup startup.
202 * @param iconName icon name
203 */
204 public void setIcon(final String iconName) {
205 this.iconName = iconName;
206 if (!TaggingPresetReader.isLoadIcons()) {
207 return;
208 }
209 File arch = TaggingPresetReader.getZipIcons();
210 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null);
211 ImageProvider imgProv = new ImageProvider(iconName);
212 imgProv.setDirs(s);
213 imgProv.setId("presets");
214 imgProv.setArchive(arch);
215 imgProv.setOptional(true);
216 imgProv.getResourceAsync().thenAccept(result -> {
217 if (result != null) {
218 GuiHelper.runInEDT(() -> result.attachImageIcon(this));
219 } else {
220 Logging.warn(toString() + ": " + PRESET_ICON_ERROR_MSG_PREFIX + iconName);
221 }
222 });
223 }
224
225 /**
226 * Called from the XML parser to set the types this preset affects.
227 * @param types comma-separated primitive types ("node", "way", "relation" or "closedway")
228 * @throws SAXException if any SAX error occurs
229 * @see TaggingPresetType#fromString
230 */
231 public void setType(String types) throws SAXException {
232 this.types = TaggingPresetItem.getType(types);
233 }
234
235 public void setName_template(String pattern) throws SAXException {
236 try {
237 this.nameTemplate = new TemplateParser(pattern).parse();
238 } catch (ParseError e) {
239 Logging.error("Error while parsing " + pattern + ": " + e.getMessage());
240 throw new SAXException(e);
241 }
242 }
243
244 public void setName_template_filter(String filter) throws SAXException {
245 try {
246 this.nameTemplateFilter = SearchCompiler.compile(filter);
247 } catch (SearchCompiler.ParseError e) {
248 Logging.error("Error while parsing" + filter + ": " + e.getMessage());
249 throw new SAXException(e);
250 }
251 }
252
253 private static class PresetPanel extends JPanel {
254 private boolean hasElements;
255
256 PresetPanel() {
257 super(new GridBagLayout());
258 }
259 }
260
261 /**
262 * Returns the tags being directly applied (without UI element) by {@link Key} items
263 *
264 * @return a list of tags
265 */
266 private List<Tag> getDirectlyAppliedTags() {
267 List<Tag> tags = new ArrayList<>();
268 for (TaggingPresetItem item : data) {
269 if (item instanceof Key) {
270 tags.add(((Key) item).asTag());
271 }
272 }
273 return tags;
274 }
275
276 /**
277 * Creates a panel for this preset. This includes general information such as name and supported {@link TaggingPresetType types}.
278 * This includes the elements from the individual {@link TaggingPresetItem items}.
279 *
280 * @param selected the selected primitives
281 * @return the newly created panel
282 */
283 public PresetPanel createPanel(Collection<OsmPrimitive> selected) {
284 PresetPanel p = new PresetPanel();
285 List<Link> l = new LinkedList<>();
286 List<PresetLink> presetLink = new LinkedList<>();
287
288 final JPanel pp = new JPanel();
289 if (types != null) {
290 for (TaggingPresetType t : types) {
291 JLabel la = new JLabel(ImageProvider.get(t.getIconName()));
292 la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName())));
293 pp.add(la);
294 }
295 }
296 final List<Tag> directlyAppliedTags = getDirectlyAppliedTags();
297 if (!directlyAppliedTags.isEmpty()) {
298 final JLabel label = new JLabel(ImageProvider.get("pastetags"));
299 label.setToolTipText("<html>" + tr("This preset also sets: {0}", Utils.joinAsHtmlUnorderedList(directlyAppliedTags)));
300 pp.add(label);
301 }
302 final int count = pp.getComponentCount();
303 if (preset_name_label) {
304 p.add(new JLabel(getIcon(Action.LARGE_ICON_KEY)), GBC.std(0, 0).span(1, count > 0 ? 2 : 1).insets(0, 0, 5, 0));
305 }
306 if (count > 0) {
307 p.add(pp, GBC.std(1, 0).span(GBC.REMAINDER));
308 }
309 if (preset_name_label) {
310 p.add(new JLabel(getName()), GBC.std(1, count > 0 ? 1 : 0).insets(5, 0, 0, 0).span(GBC.REMAINDER).fill(GBC.HORIZONTAL));
311 }
312
313 boolean presetInitiallyMatches = !selected.isEmpty() && selected.stream().allMatch(this);
314 JPanel items = new JPanel(new GridBagLayout());
315 for (TaggingPresetItem i : data) {
316 if (i instanceof Link) {
317 l.add((Link) i);
318 p.hasElements = true;
319 } else if (i instanceof PresetLink) {
320 presetLink.add((PresetLink) i);
321 } else {
322 if (i.addToPanel(items, selected, presetInitiallyMatches)) {
323 p.hasElements = true;
324 }
325 }
326 }
327 p.add(items, GBC.eol().fill());
328 if (selected.isEmpty() && !supportsRelation()) {
329 GuiHelper.setEnabledRec(items, false);
330 }
331
332 // add PresetLink
333 if (!presetLink.isEmpty()) {
334 p.add(new JLabel(tr("Edit also …")), GBC.eol().insets(0, 8, 0, 0));
335 for (PresetLink link : presetLink) {
336 link.addToPanel(p, selected, presetInitiallyMatches);
337 }
338 }
339
340 // add Link
341 for (Link link : l) {
342 link.addToPanel(p, selected, presetInitiallyMatches);
343 }
344
345 // "Add toolbar button"
346 JToggleButton tb = new JToggleButton(new ToolbarButtonAction());
347 tb.setFocusable(false);
348 p.add(tb, GBC.std(1, 0).anchor(GBC.LINE_END));
349 return p;
350 }
351
352 /**
353 * Determines whether a dialog can be shown for this preset, i.e., at least one tag can/must be set by the user.
354 *
355 * @return {@code true} if a dialog can be shown for this preset
356 */
357 public boolean isShowable() {
358 for (TaggingPresetItem i : data) {
359 if (!(i instanceof Optional || i instanceof Space || i instanceof Key))
360 return true;
361 }
362 return false;
363 }
364
365 public String suggestRoleForOsmPrimitive(OsmPrimitive osm) {
366 if (roles != null && osm != null) {
367 for (Role i : roles.roles) {
368 if (i.memberExpression != null && i.memberExpression.match(osm)
369 && (i.types == null || i.types.isEmpty() || i.types.contains(TaggingPresetType.forPrimitive(osm)))) {
370 return i.key;
371 }
372 }
373 }
374 return null;
375 }
376
377 @Override
378 public void actionPerformed(ActionEvent e) {
379 if (Main.main == null) {
380 return;
381 }
382 DataSet ds = Main.getLayerManager().getEditDataSet();
383 Collection<OsmPrimitive> participants = Collections.emptyList();
384 if (Main.main != null && ds != null) {
385 participants = ds.getSelected();
386 }
387
388 // Display dialog even if no data layer (used by preset-tagging-tester plugin)
389 Collection<OsmPrimitive> sel = createSelection(participants);
390 int answer = showDialog(sel, supportsRelation());
391
392 if (ds == null) {
393 return;
394 }
395
396 if (!sel.isEmpty() && answer == DIALOG_ANSWER_APPLY) {
397 Command cmd = createCommand(sel, getChangedTags());
398 if (cmd != null) {
399 Main.main.undoRedo.add(cmd);
400 }
401 } else if (answer == DIALOG_ANSWER_NEW_RELATION) {
402 final Relation r = new Relation();
403 final Collection<RelationMember> members = new HashSet<>();
404 for (Tag t : getChangedTags()) {
405 r.put(t.getKey(), t.getValue());
406 }
407 for (OsmPrimitive osm : ds.getSelected()) {
408 String role = suggestRoleForOsmPrimitive(osm);
409 RelationMember rm = new RelationMember(role == null ? "" : role, osm);
410 r.addMember(rm);
411 members.add(rm);
412 }
413 SwingUtilities.invokeLater(() -> RelationEditor.getEditor(Main.getLayerManager().getEditLayer(), r, members).setVisible(true));
414 }
415 ds.setSelected(ds.getSelected()); // force update
416 }
417
418 private static class PresetDialog extends ExtendedDialog {
419
420 /**
421 * Constructs a new {@code PresetDialog}.
422 * @param content the content that will be displayed in this dialog
423 * @param title the text that will be shown in the window titlebar
424 * @param icon the image to be displayed as the icon for this window
425 * @param disableApply whether to disable "Apply" button
426 * @param showNewRelation whether to display "New relation" button
427 */
428 PresetDialog(Component content, String title, ImageIcon icon, boolean disableApply, boolean showNewRelation) {
429 super(Main.parent, title,
430 showNewRelation ?
431 (new String[] {tr("Apply Preset"), tr("New relation"), tr("Cancel")}) :
432 (new String[] {tr("Apply Preset"), tr("Cancel")}),
433 true);
434 if (icon != null)
435 setIconImage(icon.getImage());
436 contentInsets = new Insets(10, 5, 0, 5);
437 if (showNewRelation) {
438 setButtonIcons("ok", "dialogs/addrelation", "cancel");
439 } else {
440 setButtonIcons("ok", "cancel");
441 }
442 setContent(content);
443 setDefaultButton(1);
444 setupDialog();
445 buttons.get(0).setEnabled(!disableApply);
446 buttons.get(0).setToolTipText(title);
447 // Prevent dialogs of being too narrow (fix #6261)
448 Dimension d = getSize();
449 if (d.width < 350) {
450 d.width = 350;
451 setSize(d);
452 }
453 super.showDialog();
454 }
455 }
456
457 /**
458 * Shows the preset dialog.
459 * @param sel selection
460 * @param showNewRelation whether to display "New relation" button
461 * @return the user choice after the dialog has been closed
462 */
463 public int showDialog(Collection<OsmPrimitive> sel, boolean showNewRelation) {
464 PresetPanel p = createPanel(sel);
465
466 int answer = 1;
467 boolean canCreateRelation = types == null || types.contains(TaggingPresetType.RELATION);
468 if (originalSelectionEmpty && !canCreateRelation) {
469 new Notification(
470 tr("The preset <i>{0}</i> cannot be applied since nothing has been selected!", getLocaleName()))
471 .setIcon(JOptionPane.WARNING_MESSAGE)
472 .show();
473 return DIALOG_ANSWER_CANCEL;
474 } else if (sel.isEmpty() && !canCreateRelation) {
475 new Notification(
476 tr("The preset <i>{0}</i> cannot be applied since the selection is unsuitable!", getLocaleName()))
477 .setIcon(JOptionPane.WARNING_MESSAGE)
478 .show();
479 return DIALOG_ANSWER_CANCEL;
480 } else if (p.getComponentCount() != 0 && (sel.isEmpty() || p.hasElements)) {
481 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
482 if (sel.isEmpty()) {
483 if (originalSelectionEmpty) {
484 title = tr("Nothing selected!");
485 } else {
486 title = tr("Selection unsuitable!");
487 }
488 }
489
490 answer = new PresetDialog(p, title, preset_name_label ? null : (ImageIcon) getValue(Action.SMALL_ICON),
491 sel.isEmpty(), showNewRelation).getValue();
492 }
493 if (!showNewRelation && answer == 2)
494 return DIALOG_ANSWER_CANCEL;
495 else
496 return answer;
497 }
498
499 /**
500 * Removes all unsuitable OsmPrimitives from the given list
501 * @param participants List of possible OsmPrimitives to tag
502 * @return Cleaned list with suitable OsmPrimitives only
503 */
504 public Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) {
505 originalSelectionEmpty = participants.isEmpty();
506 Collection<OsmPrimitive> sel = new LinkedList<>();
507 for (OsmPrimitive osm : participants) {
508 if (typeMatches(EnumSet.of(TaggingPresetType.forPrimitive(osm)))) {
509 sel.add(osm);
510 }
511 }
512 return sel;
513 }
514
515 /**
516 * Gets a list of tags that are set by this preset.
517 * @return The list of tags.
518 */
519 public List<Tag> getChangedTags() {
520 List<Tag> result = new ArrayList<>();
521 for (TaggingPresetItem i: data) {
522 i.addCommands(result);
523 }
524 return result;
525 }
526
527 /**
528 * Create a command to change the given list of tags.
529 * @param sel The primitives to change the tags for
530 * @param changedTags The tags to change
531 * @return A command that changes the tags.
532 */
533 public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) {
534 List<Command> cmds = new ArrayList<>();
535 for (Tag tag: changedTags) {
536 ChangePropertyCommand cmd = new ChangePropertyCommand(sel, tag.getKey(), tag.getValue());
537 if (cmd.getObjectsNumber() > 0) {
538 cmds.add(cmd);
539 }
540 }
541
542 if (cmds.isEmpty())
543 return null;
544 else if (cmds.size() == 1)
545 return cmds.get(0);
546 else
547 return new SequenceCommand(tr("Change Tags"), cmds);
548 }
549
550 private boolean supportsRelation() {
551 return types == null || types.contains(TaggingPresetType.RELATION);
552 }
553
554 protected final void updateEnabledState() {
555 setEnabled(Main.main != null && Main.getLayerManager().getEditDataSet() != null);
556 }
557
558 @Override
559 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
560 updateEnabledState();
561 }
562
563 @Override
564 public String toString() {
565 return (types == null ? "" : types.toString()) + ' ' + name;
566 }
567
568 /**
569 * Determines whether this preset matches the types.
570 * @param t The types that must match
571 * @return <code>true</code> if all types match.
572 */
573 public boolean typeMatches(Collection<TaggingPresetType> t) {
574 return t == null || types == null || types.containsAll(t);
575 }
576
577 /**
578 * Determines whether this preset matches the given primitive, i.e.,
579 * whether the {@link #typeMatches(Collection) type matches} and the {@link TaggingPresetItem#matches(Map) tags match}.
580 *
581 * @param p the primitive
582 * @return {@code true} if this preset matches the primitive
583 */
584 @Override
585 public boolean test(OsmPrimitive p) {
586 return matches(EnumSet.of(TaggingPresetType.forPrimitive(p)), p.getKeys(), false);
587 }
588
589 /**
590 * Determines whether this preset matches the parameters.
591 *
592 * @param t the preset types to include, see {@link #typeMatches(Collection)}
593 * @param tags the tags to perform matching on, see {@link TaggingPresetItem#matches(Map)}
594 * @param onlyShowable whether the preset must be {@link #isShowable() showable}
595 * @return {@code true} if this preset matches the parameters.
596 */
597 public boolean matches(Collection<TaggingPresetType> t, Map<String, String> tags, boolean onlyShowable) {
598 if ((onlyShowable && !isShowable()) || !typeMatches(t)) {
599 return false;
600 } else {
601 return TaggingPresetItem.matches(data, tags);
602 }
603 }
604
605 /**
606 * Action that adds or removes the button on main toolbar
607 */
608 public class ToolbarButtonAction extends AbstractAction {
609 private final int toolbarIndex;
610
611 /**
612 * Constructs a new {@code ToolbarButtonAction}.
613 */
614 public ToolbarButtonAction() {
615 super("", ImageProvider.get("dialogs", "pin"));
616 putValue(SHORT_DESCRIPTION, tr("Add or remove toolbar button"));
617 List<String> t = new LinkedList<>(ToolbarPreferences.getToolString());
618 toolbarIndex = t.indexOf(getToolbarString());
619 putValue(SELECTED_KEY, toolbarIndex >= 0);
620 }
621
622 @Override
623 public void actionPerformed(ActionEvent ae) {
624 String res = getToolbarString();
625 Main.toolbar.addCustomButton(res, toolbarIndex, true);
626 }
627 }
628
629 /**
630 * Gets a string describing this preset that can be used for the toolbar
631 * @return A String that can be passed on to the toolbar
632 * @see ToolbarPreferences#addCustomButton(String, int, boolean)
633 */
634 public String getToolbarString() {
635 ToolbarPreferences.ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
636 return actionParser.saveAction(new ToolbarPreferences.ActionDefinition(this));
637 }
638}
Note: See TracBrowser for help on using the repository browser.