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

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

improve unit test detecting invalid presets

  • Property svn:eol-style set to native
File size: 20.5 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;
23
24import javax.swing.AbstractAction;
25import javax.swing.Action;
26import javax.swing.ImageIcon;
27import javax.swing.JLabel;
28import javax.swing.JOptionPane;
29import javax.swing.JPanel;
30import javax.swing.JToggleButton;
31import javax.swing.SwingUtilities;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.actions.search.SearchCompiler;
35import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
36import org.openstreetmap.josm.command.ChangePropertyCommand;
37import org.openstreetmap.josm.command.Command;
38import org.openstreetmap.josm.command.SequenceCommand;
39import org.openstreetmap.josm.data.osm.DataSet;
40import org.openstreetmap.josm.data.osm.OsmPrimitive;
41import org.openstreetmap.josm.data.osm.Relation;
42import org.openstreetmap.josm.data.osm.RelationMember;
43import org.openstreetmap.josm.data.osm.Tag;
44import org.openstreetmap.josm.gui.ExtendedDialog;
45import org.openstreetmap.josm.gui.MapView;
46import org.openstreetmap.josm.gui.Notification;
47import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
48import org.openstreetmap.josm.gui.layer.Layer;
49import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
50import org.openstreetmap.josm.gui.tagging.presets.items.Key;
51import org.openstreetmap.josm.gui.tagging.presets.items.Label;
52import org.openstreetmap.josm.gui.tagging.presets.items.Link;
53import org.openstreetmap.josm.gui.tagging.presets.items.Optional;
54import org.openstreetmap.josm.gui.tagging.presets.items.PresetLink;
55import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
56import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
57import org.openstreetmap.josm.gui.tagging.presets.items.Space;
58import org.openstreetmap.josm.gui.util.GuiHelper;
59import org.openstreetmap.josm.tools.GBC;
60import org.openstreetmap.josm.tools.ImageProvider;
61import org.openstreetmap.josm.tools.ImageResource;
62import org.openstreetmap.josm.tools.Predicate;
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 MapView.LayerChangeListener, 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 public TaggingPresetMenu group;
89 public String name;
90 public String iconName;
91 public String name_context;
92 public String locale_name;
93 public boolean preset_name_label;
94
95 /**
96 * The types as preparsed collection.
97 */
98 public Set<TaggingPresetType> types;
99 public transient List<TaggingPresetItem> data = new LinkedList<>();
100 public transient Roles roles;
101 public transient TemplateEntry nameTemplate;
102 public transient Match nameTemplateFilter;
103
104 /**
105 * True whenever the original selection given into createSelection was empty
106 */
107 private boolean originalSelectionEmpty;
108
109 /**
110 * Create an empty tagging preset. This will not have any items and
111 * will be an empty string as text. createPanel will return null.
112 * Use this as default item for "do not select anything".
113 */
114 public TaggingPreset() {
115 MapView.addLayerChangeListener(this);
116 updateEnabledState();
117 }
118
119 /**
120 * Change the display name without changing the toolbar value.
121 */
122 public void setDisplayName() {
123 putValue(Action.NAME, getName());
124 putValue("toolbar", "tagging_" + getRawName());
125 putValue(OPTIONAL_TOOLTIP_TEXT, group != null ?
126 tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) :
127 tr("Use preset ''{0}''", getLocaleName()));
128 }
129
130 public String getLocaleName() {
131 if (locale_name == null) {
132 if (name_context != null) {
133 locale_name = trc(name_context, TaggingPresetItem.fixPresetString(name));
134 } else {
135 locale_name = tr(TaggingPresetItem.fixPresetString(name));
136 }
137 }
138 return locale_name;
139 }
140
141 /**
142 * Returns the translated name of this preset, prefixed with the group names it belongs to.
143 * @return the translated name of this preset, prefixed with the group names it belongs to
144 */
145 public String getName() {
146 return group != null ? group.getName() + '/' + getLocaleName() : getLocaleName();
147 }
148
149 /**
150 * Returns the non translated name of this preset, prefixed with the (non translated) group names it belongs to.
151 * @return the non translated name of this preset, prefixed with the (non translated) group names it belongs to
152 */
153 public String getRawName() {
154 return group != null ? group.getRawName() + '/' + name : name;
155 }
156
157 /**
158 * Returns the preset icon.
159 * @return The preset icon, or {@code null} if none defined
160 * @since 6403
161 */
162 public final ImageIcon getIcon() {
163 Object icon = getValue(Action.SMALL_ICON);
164 if (icon instanceof ImageIcon) {
165 return (ImageIcon) icon;
166 }
167 return null;
168 }
169
170 /**
171 * Called from the XML parser to set the icon.
172 * The loading task is performed in the background in order to speedup startup.
173 * @param iconName icon name
174 */
175 public void setIcon(final String iconName) {
176 this.iconName = iconName;
177 if (!TaggingPresetReader.isLoadIcons()) {
178 return;
179 }
180 File arch = TaggingPresetReader.getZipIcons();
181 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null);
182 ImageProvider imgProv = new ImageProvider(iconName);
183 imgProv.setDirs(s);
184 imgProv.setId("presets");
185 imgProv.setArchive(arch);
186 imgProv.setOptional(true);
187 imgProv.getInBackground(new ImageProvider.ImageResourceCallback() {
188 @Override
189 public void finished(final ImageResource result) {
190 if (result != null) {
191 GuiHelper.runInEDT(new Runnable() {
192 @Override
193 public void run() {
194 result.getImageIcon(TaggingPreset.this);
195 }
196 });
197 } else {
198 Main.warn(TaggingPreset.this + ": " + PRESET_ICON_ERROR_MSG_PREFIX + iconName);
199 }
200 }
201 });
202 }
203
204 /**
205 * Called from the XML parser to set the types this preset affects.
206 * @throws SAXException if any SAX error occurs
207 */
208 public void setType(String types) throws SAXException {
209 this.types = TaggingPresetItem.getType(types);
210 }
211
212 public void setName_template(String pattern) throws SAXException {
213 try {
214 this.nameTemplate = new TemplateParser(pattern).parse();
215 } catch (ParseError e) {
216 Main.error("Error while parsing " + pattern + ": " + e.getMessage());
217 throw new SAXException(e);
218 }
219 }
220
221 public void setName_template_filter(String filter) throws SAXException {
222 try {
223 this.nameTemplateFilter = SearchCompiler.compile(filter);
224 } catch (org.openstreetmap.josm.actions.search.SearchCompiler.ParseError e) {
225 Main.error("Error while parsing" + filter + ": " + e.getMessage());
226 throw new SAXException(e);
227 }
228 }
229
230 private static class PresetPanel extends JPanel {
231 private boolean hasElements;
232
233 PresetPanel() {
234 super(new GridBagLayout());
235 }
236 }
237
238 public PresetPanel createPanel(Collection<OsmPrimitive> selected) {
239 if (data == null)
240 return null;
241 PresetPanel p = new PresetPanel();
242 List<Link> l = new LinkedList<>();
243 List<PresetLink> presetLink = new LinkedList<>();
244 if (types != null) {
245 JPanel pp = new JPanel();
246 for (TaggingPresetType t : types) {
247 JLabel la = new JLabel(ImageProvider.get(t.getIconName()));
248 la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName())));
249 pp.add(la);
250 }
251 p.add(pp, GBC.eol());
252 }
253 if (preset_name_label) {
254 Label.addLabel(p, getIcon(), getName());
255 }
256
257 boolean presetInitiallyMatches = !selected.isEmpty() && Utils.forAll(selected, this);
258 JPanel items = new JPanel(new GridBagLayout());
259 for (TaggingPresetItem i : data) {
260 if (i instanceof Link) {
261 l.add((Link) i);
262 p.hasElements = true;
263 } else if (i instanceof PresetLink) {
264 presetLink.add((PresetLink) i);
265 } else {
266 if (i.addToPanel(items, selected, presetInitiallyMatches)) {
267 p.hasElements = true;
268 }
269 }
270 }
271 p.add(items, GBC.eol().fill());
272 if (selected.isEmpty() && !supportsRelation()) {
273 GuiHelper.setEnabledRec(items, false);
274 }
275
276 // add PresetLink
277 if (!presetLink.isEmpty()) {
278 p.add(new JLabel(tr("Edit also …")), GBC.eol().insets(0, 8, 0, 0));
279 for (PresetLink link : presetLink) {
280 link.addToPanel(p, selected, presetInitiallyMatches);
281 }
282 }
283
284 // add Link
285 for (Link link : l) {
286 link.addToPanel(p, selected, presetInitiallyMatches);
287 }
288
289 // "Add toolbar button"
290 JToggleButton tb = new JToggleButton(new ToolbarButtonAction());
291 tb.setFocusable(false);
292 p.add(tb, GBC.std(0, 0).anchor(GBC.LINE_END));
293 return p;
294 }
295
296 public boolean isShowable() {
297 for (TaggingPresetItem i : data) {
298 if (!(i instanceof Optional || i instanceof Space || i instanceof Key))
299 return true;
300 }
301 return false;
302 }
303
304 public String suggestRoleForOsmPrimitive(OsmPrimitive osm) {
305 if (roles != null && osm != null) {
306 for (Role i : roles.roles) {
307 if (i.memberExpression != null && i.memberExpression.match(osm)
308 && (i.types == null || i.types.isEmpty() || i.types.contains(TaggingPresetType.forPrimitive(osm)))) {
309 return i.key;
310 }
311 }
312 }
313 return null;
314 }
315
316 @Override
317 public void actionPerformed(ActionEvent e) {
318 if (Main.main == null) {
319 return;
320 }
321 DataSet ds = Main.main.getCurrentDataSet();
322 Collection<OsmPrimitive> participants = Collections.emptyList();
323 if (Main.main != null && ds != null) {
324 participants = ds.getSelected();
325 }
326
327 // Display dialog even if no data layer (used by preset-tagging-tester plugin)
328 Collection<OsmPrimitive> sel = createSelection(participants);
329 int answer = showDialog(sel, supportsRelation());
330
331 if (ds == null) {
332 return;
333 }
334
335 if (!sel.isEmpty() && answer == DIALOG_ANSWER_APPLY) {
336 Command cmd = createCommand(sel, getChangedTags());
337 if (cmd != null) {
338 Main.main.undoRedo.add(cmd);
339 }
340 } else if (answer == DIALOG_ANSWER_NEW_RELATION) {
341 final Relation r = new Relation();
342 final Collection<RelationMember> members = new HashSet<>();
343 for (Tag t : getChangedTags()) {
344 r.put(t.getKey(), t.getValue());
345 }
346 for (OsmPrimitive osm : ds.getSelected()) {
347 String role = suggestRoleForOsmPrimitive(osm);
348 RelationMember rm = new RelationMember(role == null ? "" : role, osm);
349 r.addMember(rm);
350 members.add(rm);
351 }
352 SwingUtilities.invokeLater(new Runnable() {
353 @Override
354 public void run() {
355 RelationEditor.getEditor(Main.main.getEditLayer(), r, members).setVisible(true);
356 }
357 });
358 }
359 ds.setSelected(ds.getSelected()); // force update
360 }
361
362 private static class PresetDialog extends ExtendedDialog {
363 PresetDialog(Component content, String title, ImageIcon icon, boolean disableApply, boolean showNewRelation) {
364 super(Main.parent, title,
365 showNewRelation ?
366 new String[] {tr("Apply Preset"), tr("New relation"), tr("Cancel")} :
367 new String[] {tr("Apply Preset"), tr("Cancel")},
368 true);
369 if (icon != null)
370 setIconImage(icon.getImage());
371 contentInsets = new Insets(10, 5, 0, 5);
372 if (showNewRelation) {
373 setButtonIcons(new String[] {"ok", "dialogs/addrelation", "cancel" });
374 } else {
375 setButtonIcons(new String[] {"ok", "cancel" });
376 }
377 setContent(content);
378 setDefaultButton(1);
379 setupDialog();
380 buttons.get(0).setEnabled(!disableApply);
381 buttons.get(0).setToolTipText(title);
382 // Prevent dialogs of being too narrow (fix #6261)
383 Dimension d = getSize();
384 if (d.width < 350) {
385 d.width = 350;
386 setSize(d);
387 }
388 showDialog();
389 }
390 }
391
392 public int showDialog(Collection<OsmPrimitive> sel, boolean showNewRelation) {
393 PresetPanel p = createPanel(sel);
394 if (p == null)
395 return DIALOG_ANSWER_CANCEL;
396
397 int answer = 1;
398 boolean canCreateRelation = types == null || types.contains(TaggingPresetType.RELATION);
399 if (originalSelectionEmpty && !canCreateRelation) {
400 new Notification(
401 tr("The preset <i>{0}</i> cannot be applied since nothing has been selected!", getLocaleName()))
402 .setIcon(JOptionPane.WARNING_MESSAGE)
403 .show();
404 return DIALOG_ANSWER_CANCEL;
405 } else if (sel.isEmpty() && !canCreateRelation) {
406 new Notification(
407 tr("The preset <i>{0}</i> cannot be applied since the selection is unsuitable!", getLocaleName()))
408 .setIcon(JOptionPane.WARNING_MESSAGE)
409 .show();
410 return DIALOG_ANSWER_CANCEL;
411 } else if (p.getComponentCount() != 0 && (sel.isEmpty() || p.hasElements)) {
412 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
413 if (sel.isEmpty()) {
414 if (originalSelectionEmpty) {
415 title = tr("Nothing selected!");
416 } else {
417 title = tr("Selection unsuitable!");
418 }
419 }
420
421 answer = new PresetDialog(p, title, preset_name_label ? null : (ImageIcon) getValue(Action.SMALL_ICON),
422 sel.isEmpty(), showNewRelation).getValue();
423 }
424 if (!showNewRelation && answer == 2)
425 return DIALOG_ANSWER_CANCEL;
426 else
427 return answer;
428 }
429
430 /**
431 * Removes all unsuitable OsmPrimitives from the given list
432 * @param participants List of possible OsmPrimitives to tag
433 * @return Cleaned list with suitable OsmPrimitives only
434 */
435 public Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) {
436 originalSelectionEmpty = participants.isEmpty();
437 Collection<OsmPrimitive> sel = new LinkedList<>();
438 for (OsmPrimitive osm : participants) {
439 if (typeMatches(EnumSet.of(TaggingPresetType.forPrimitive(osm)))) {
440 sel.add(osm);
441 }
442 }
443 return sel;
444 }
445
446 public List<Tag> getChangedTags() {
447 List<Tag> result = new ArrayList<>();
448 for (TaggingPresetItem i: data) {
449 i.addCommands(result);
450 }
451 return result;
452 }
453
454 public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) {
455 List<Command> cmds = new ArrayList<>();
456 for (Tag tag: changedTags) {
457 ChangePropertyCommand cmd = new ChangePropertyCommand(sel, tag.getKey(), tag.getValue());
458 if (cmd.getObjectsNumber() > 0) {
459 cmds.add(cmd);
460 }
461 }
462
463 if (cmds.isEmpty())
464 return null;
465 else if (cmds.size() == 1)
466 return cmds.get(0);
467 else
468 return new SequenceCommand(tr("Change Tags"), cmds);
469 }
470
471 private boolean supportsRelation() {
472 return types == null || types.contains(TaggingPresetType.RELATION);
473 }
474
475 protected final void updateEnabledState() {
476 setEnabled(Main.main != null && Main.main.getCurrentDataSet() != null);
477 }
478
479 @Override
480 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
481 updateEnabledState();
482 }
483
484 @Override
485 public void layerAdded(Layer newLayer) {
486 updateEnabledState();
487 }
488
489 @Override
490 public void layerRemoved(Layer oldLayer) {
491 updateEnabledState();
492 }
493
494 @Override
495 public String toString() {
496 return (types == null ? "" : types) + " " + name;
497 }
498
499 public boolean typeMatches(Collection<TaggingPresetType> t) {
500 return t == null || types == null || types.containsAll(t);
501 }
502
503 @Override
504 public boolean evaluate(OsmPrimitive p) {
505 return matches(EnumSet.of(TaggingPresetType.forPrimitive(p)), p.getKeys(), false);
506 }
507
508 public boolean matches(Collection<TaggingPresetType> t, Map<String, String> tags, boolean onlyShowable) {
509 if (onlyShowable && !isShowable())
510 return false;
511 else if (!typeMatches(t))
512 return false;
513 boolean atLeastOnePositiveMatch = false;
514 for (TaggingPresetItem item : data) {
515 Boolean m = item.matches(tags);
516 if (m != null && !m)
517 return false;
518 else if (m != null) {
519 atLeastOnePositiveMatch = true;
520 }
521 }
522 return atLeastOnePositiveMatch;
523 }
524
525 public static Collection<TaggingPreset> getMatchingPresets(final Collection<TaggingPresetType> t,
526 final Map<String, String> tags, final boolean onlyShowable) {
527 return Utils.filter(TaggingPresets.getTaggingPresets(), new Predicate<TaggingPreset>() {
528 @Override
529 public boolean evaluate(TaggingPreset object) {
530 return object.matches(t, tags, onlyShowable);
531 }
532 });
533 }
534
535 /**
536 * Action that adds or removes the button on main toolbar
537 */
538 public class ToolbarButtonAction extends AbstractAction {
539 private final int toolbarIndex;
540
541 /**
542 * Constructs a new {@code ToolbarButtonAction}.
543 */
544 public ToolbarButtonAction() {
545 super("", ImageProvider.get("dialogs", "pin"));
546 putValue(SHORT_DESCRIPTION, tr("Add or remove toolbar button"));
547 List<String> t = new LinkedList<>(ToolbarPreferences.getToolString());
548 toolbarIndex = t.indexOf(getToolbarString());
549 putValue(SELECTED_KEY, toolbarIndex >= 0);
550 }
551
552 @Override
553 public void actionPerformed(ActionEvent ae) {
554 String res = getToolbarString();
555 Main.toolbar.addCustomButton(res, toolbarIndex, true);
556 }
557 }
558
559 public String getToolbarString() {
560 ToolbarPreferences.ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
561 return actionParser.saveAction(new ToolbarPreferences.ActionDefinition(this));
562 }
563}
Note: See TracBrowser for help on using the repository browser.