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

Last change on this file since 1415 was 1415, checked in by stoecker, 15 years ago

applied patch #2185 by bruce89

  • Property svn:eol-style set to native
File size: 24.9 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.GridBagLayout;
9import java.awt.Image;
10import java.awt.event.ActionEvent;
11import java.io.BufferedReader;
12import java.io.InputStreamReader;
13import java.io.IOException;
14import java.io.Reader;
15import java.io.UnsupportedEncodingException;
16import java.util.Arrays;
17import java.util.Collection;
18import java.util.HashMap;
19import java.util.HashSet;
20import java.util.LinkedHashMap;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Set;
24
25import javax.swing.AbstractAction;
26import javax.swing.Action;
27import javax.swing.ImageIcon;
28import javax.swing.JComboBox;
29import javax.swing.JComponent;
30import javax.swing.JLabel;
31import javax.swing.JOptionPane;
32import javax.swing.JPanel;
33import javax.swing.JTextField;
34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.command.ChangePropertyCommand;
37import org.openstreetmap.josm.command.Command;
38import org.openstreetmap.josm.command.SequenceCommand;
39import org.openstreetmap.josm.data.osm.OsmPrimitive;
40import org.openstreetmap.josm.data.osm.OsmUtils;
41import org.openstreetmap.josm.data.osm.Node;
42import org.openstreetmap.josm.data.osm.Way;
43import org.openstreetmap.josm.data.osm.Relation;
44import org.openstreetmap.josm.io.MirroredInputStream;
45import org.openstreetmap.josm.gui.QuadStateCheckBox;
46import org.openstreetmap.josm.gui.tagging.TaggingPresetMenu;
47import org.openstreetmap.josm.gui.tagging.TaggingPresetSeparator;
48import org.openstreetmap.josm.tools.GBC;
49import org.openstreetmap.josm.tools.ImageProvider;
50import org.openstreetmap.josm.tools.UrlLabel;
51import org.openstreetmap.josm.tools.XmlObjectParser;
52import org.xml.sax.SAXException;
53
54/**
55 * This class read encapsulate one tagging preset. A class method can
56 * read in all predefined presets, either shipped with JOSM or that are
57 * in the config directory.
58 *
59 * It is also able to construct dialogs out of preset definitions.
60 */
61public class TaggingPreset extends AbstractAction {
62
63 public TaggingPresetMenu group = null;
64 public String name;
65
66 public static abstract class Item {
67 public boolean focus = false;
68 abstract void addToPanel(JPanel p, Collection<OsmPrimitive> sel);
69 abstract void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds);
70 boolean requestFocusInWindow() {return false;}
71 }
72
73 public static class Usage {
74 Set<String> values;
75 Boolean hadKeys = false;
76 Boolean hadEmpty = false;
77 public Boolean allSimilar()
78 {
79 return values.size() == 1 && !hadEmpty;
80 }
81 public Boolean unused()
82 {
83 return values.size() == 0;
84 }
85 public String getFirst()
86 {
87 return (String)(values.toArray()[0]);
88 }
89 public Boolean hadKeys()
90 {
91 return hadKeys;
92 }
93 }
94
95 public static final String DIFFERENT = tr("<different>");
96
97 static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
98 Usage returnValue = new Usage();
99 returnValue.values = new HashSet<String>();
100 for (OsmPrimitive s : sel) {
101 String v = s.get(key);
102 if (v != null)
103 returnValue.values.add(v);
104 else
105 returnValue.hadEmpty = true;
106 if(s.keys != null && s.keys.size() > 0)
107 returnValue.hadKeys = true;
108 }
109 return returnValue;
110 }
111
112 static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
113
114 Usage returnValue = new Usage();
115 returnValue.values = new HashSet<String>();
116 for (OsmPrimitive s : sel) {
117 returnValue.values.add(OsmUtils.getNamedOsmBoolean(s.get(key)));
118 }
119 return returnValue;
120 }
121
122 public static class Text extends Item {
123
124 public String key;
125 public String text;
126 public String locale_text;
127 public String default_;
128 public String originalValue;
129 public boolean use_last_as_default = false;
130 public boolean delete_if_empty = false;
131
132 private JComponent value;
133
134 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
135
136 // find out if our key is already used in the selection.
137 Usage usage = determineTextUsage(sel, key);
138 if (usage.unused())
139 {
140 value = new JTextField();
141 if (use_last_as_default && lastValue.containsKey(key)) {
142 ((JTextField)value).setText(lastValue.get(key));
143 } else {
144 ((JTextField)value).setText(default_);
145 }
146 originalValue = null;
147 } else if (usage.allSimilar()) {
148 // all objects use the same value
149 value = new JTextField();
150 for (String s : usage.values) ((JTextField) value).setText(s);
151 originalValue = ((JTextField)value).getText();
152 } else {
153 // the objects have different values
154 value = new JComboBox(usage.values.toArray());
155 ((JComboBox)value).setEditable(true);
156 ((JComboBox)value).getEditor().setItem(DIFFERENT);
157 originalValue = DIFFERENT;
158 }
159 if(locale_text == null)
160 locale_text = tr(text);
161 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
162 p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
163 }
164
165 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
166
167 // return if unchanged
168 String v = (value instanceof JComboBox) ?
169 ((JComboBox)value).getEditor().getItem().toString() :
170 ((JTextField)value).getText();
171
172 if (use_last_as_default) lastValue.put(key, v);
173 if (v.equals(originalValue) || (originalValue == null && v.length() == 0)) return;
174
175 if (delete_if_empty && v.length() == 0)
176 v = null;
177 cmds.add(new ChangePropertyCommand(sel, key, v));
178 }
179 @Override boolean requestFocusInWindow() {return value.requestFocusInWindow();}
180 }
181
182 public static class Check extends Item {
183
184 public String key;
185 public String text;
186 public String locale_text;
187 public boolean default_ = false; // only used for tagless objects
188 public boolean use_last_as_default = false;
189
190 private QuadStateCheckBox check;
191 private QuadStateCheckBox.State initialState;
192
193 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
194
195 // find out if our key is already used in the selection.
196 Usage usage = determineBooleanUsage(sel, key);
197
198 if(locale_text == null)
199 locale_text = tr(text);
200
201 String oneValue = null;
202 for (String s : usage.values) oneValue = s;
203 if (usage.values.size() < 2 && (oneValue == null || OsmUtils.trueval.equals(oneValue) || OsmUtils.falseval.equals(oneValue))) {
204 if(default_)
205 {
206 for (OsmPrimitive s : sel)
207 if(s.keys != null && s.keys.size() > 0) default_ = false;
208 }
209
210 // all selected objects share the same value which is either true or false or unset,
211 // we can display a standard check box.
212 initialState = OsmUtils.trueval.equals(oneValue) ?
213 QuadStateCheckBox.State.SELECTED :
214 OsmUtils.falseval.equals(oneValue) ?
215 QuadStateCheckBox.State.NOT_SELECTED :
216 default_ ? QuadStateCheckBox.State.SELECTED
217 : QuadStateCheckBox.State.UNSET;
218 check = new QuadStateCheckBox(locale_text, initialState,
219 new QuadStateCheckBox.State[] {
220 QuadStateCheckBox.State.SELECTED,
221 QuadStateCheckBox.State.NOT_SELECTED,
222 QuadStateCheckBox.State.UNSET });
223 } else {
224 // the objects have different values, or one or more objects have something
225 // else than true/false. we display a quad-state check box
226 // in "partial" state.
227 initialState = QuadStateCheckBox.State.PARTIAL;
228 check = new QuadStateCheckBox(locale_text, QuadStateCheckBox.State.PARTIAL,
229 new QuadStateCheckBox.State[] {
230 QuadStateCheckBox.State.PARTIAL,
231 QuadStateCheckBox.State.SELECTED,
232 QuadStateCheckBox.State.NOT_SELECTED,
233 QuadStateCheckBox.State.UNSET });
234 }
235 p.add(check, GBC.eol().fill(GBC.HORIZONTAL));
236 }
237
238 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
239 // if the user hasn't changed anything, don't create a command.
240 if (check.getState() == initialState) return;
241
242 // otherwise change things according to the selected value.
243 cmds.add(new ChangePropertyCommand(sel, key,
244 check.getState() == QuadStateCheckBox.State.SELECTED ? OsmUtils.trueval :
245 check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? OsmUtils.falseval :
246 null));
247 }
248 @Override boolean requestFocusInWindow() {return check.requestFocusInWindow();}
249 }
250
251 public static class Combo extends Item {
252
253 public String key;
254 public String text;
255 public String locale_text;
256 public String values;
257 public String display_values;
258 public String locale_display_values;
259 public String default_;
260 public boolean delete_if_empty = false;
261 public boolean editable = true;
262 public boolean use_last_as_default = false;
263
264 private JComboBox combo;
265 private LinkedHashMap<String,String> lhm;
266 private Usage usage;
267 private String originalValue;
268
269 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
270
271 // find out if our key is already used in the selection.
272 usage = determineTextUsage(sel, key);
273
274 String[] value_array = values.split(",");
275 String[] display_array;
276 if(locale_display_values != null)
277 display_array = locale_display_values.split(",");
278 else if(display_values != null)
279 display_array = display_values.split(",");
280 else
281 display_array = value_array;
282
283 lhm = new LinkedHashMap<String,String>();
284 if (!usage.allSimilar() && !usage.unused())
285 {
286 lhm.put(DIFFERENT, DIFFERENT);
287 }
288 for (int i=0; i<value_array.length; i++) {
289 lhm.put(value_array[i],
290 (locale_display_values == null) ?
291 tr(display_array[i]) : display_array[i]);
292 }
293 if(!usage.unused())
294 {
295 for (String s : usage.values) {
296 if (!lhm.containsKey(s)) lhm.put(s, s);
297 }
298 }
299 if (default_ != null && !lhm.containsKey(default_)) lhm.put(default_, default_);
300 if(!lhm.containsKey("")) lhm.put("", "");
301
302 combo = new JComboBox(lhm.values().toArray());
303 combo.setEditable(editable);
304 if (usage.allSimilar() && !usage.unused())
305 {
306 originalValue=usage.getFirst();
307 combo.setSelectedItem(lhm.get(originalValue));
308 }
309 // use default only in case it is a totally new entry
310 else if(default_ != null && !usage.hadKeys())
311 {
312 combo.setSelectedItem(default_);
313 originalValue=DIFFERENT;
314 }
315 else if(usage.unused())
316 {
317 combo.setSelectedItem("");
318 originalValue="";
319 }
320 else
321 {
322 combo.setSelectedItem(DIFFERENT);
323 originalValue=DIFFERENT;
324 }
325
326 if(locale_text == null)
327 locale_text = tr(text);
328 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
329 p.add(combo, GBC.eol().fill(GBC.HORIZONTAL));
330 }
331 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
332 Object obj = combo.getSelectedItem();
333 String display = (obj == null) ? null : obj.toString();
334 String value = null;
335 if(display == null && combo.isEditable())
336 display = combo.getEditor().getItem().toString();
337
338 if (display != null)
339 {
340 for (String key : lhm.keySet()) {
341 String k = lhm.get(key);
342 if (k != null && k.equals(display)) value=key;
343 }
344 if(value == null)
345 value = display;
346 }
347 else
348 value = "";
349
350 // no change if same as before
351 if (value.equals(originalValue) || (originalValue == null && (value == null || value.length() == 0))) return;
352
353 if (delete_if_empty && value != null && value.length() == 0)
354 value = null;
355 cmds.add(new ChangePropertyCommand(sel, key, value));
356 }
357 @Override boolean requestFocusInWindow() {return combo.requestFocusInWindow();}
358 }
359
360 public static class Label extends Item {
361 public String text;
362 public String locale_text;
363
364 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
365 if(locale_text == null)
366 locale_text = tr(text);
367 p.add(new JLabel(locale_text), GBC.eol());
368 }
369 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
370 }
371
372 public static class Link extends Item {
373 public String href;
374 public String text;
375 public String locale_text;
376 public String locale_href;
377
378 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
379 if(locale_text == null)
380 locale_text = text == null ? tr("More information about this feature") : tr(text);
381 p.add(new UrlLabel(locale_href, locale_text), GBC.eol().anchor(GBC.WEST));
382 }
383 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
384 }
385
386 public static class Optional extends Item {
387 // TODO: Draw a box around optional stuff
388 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
389 p.add(new JLabel(" "), GBC.eol()); // space
390 p.add(new JLabel(tr("Optional Attributes:")), GBC.eol());
391 p.add(new JLabel(" "), GBC.eol()); // space
392 }
393 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
394 }
395
396 public static class Space extends Item {
397 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
398 p.add(new JLabel(" "), GBC.eol()); // space
399 }
400 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
401 }
402
403 public static class Key extends Item {
404 public String key;
405 public String value;
406
407 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) { }
408 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
409 cmds.add(new ChangePropertyCommand(sel, key, value != null && !value.equals("") ? value : null));
410 }
411 }
412
413 /**
414 * The types as preparsed collection.
415 */
416 public List<String> types;
417 public List<Item> data = new LinkedList<Item>();
418 private static HashMap<String,String> lastValue = new HashMap<String,String>();
419
420 /**
421 * Create an empty tagging preset. This will not have any items and
422 * will be an empty string as text. createPanel will return null.
423 * Use this as default item for "do not select anything".
424 */
425 public TaggingPreset() {}
426
427 /**
428 * Change the display name without changing the toolbar value.
429 */
430 public void setDisplayName() {
431 putValue(Action.NAME, getName());
432 putValue("toolbar", "tagging_" + getRawName());
433 putValue(SHORT_DESCRIPTION, (group != null ?
434 tr("Use preset ''{0}'' of group ''{1}''", tr(name), group.getName()) :
435 tr("Use preset ''{0}''", tr(name))));
436 }
437
438 public String getName() {
439 return group != null ? group.getName() + "/" + tr(name) : tr(name);
440 }
441 public String getRawName() {
442 return group != null ? group.getRawName() + "/" + name : name;
443 }
444 /**
445 * Called from the XML parser to set the icon
446 *
447 * FIXME for Java 1.6 - use 24x24 icons for LARGE_ICON_KEY (button bar)
448 * and the 16x16 icons for SMALL_ICON.
449 */
450 public void setIcon(String iconName) {
451 String s = Main.pref.get("taggingpreset.iconpaths");
452 ImageIcon icon = ImageProvider.getIfAvailable((s != null ? s.split(";") : null), "presets", null, iconName);
453 if (icon == null)
454 {
455 System.out.println("Could not get presets icon " + iconName);
456 icon = new ImageIcon(iconName);
457 }
458 if (Math.max(icon.getIconHeight(), icon.getIconWidth()) != 16)
459 icon = new ImageIcon(icon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH));
460 putValue(Action.SMALL_ICON, icon);
461 }
462
463 /**
464 * Called from the XML parser to set the types this preset affects
465 */
466 private static Collection<String> allowedtypes = Arrays.asList(new String[]
467 {marktr("way"), marktr("node"), marktr("relation"), marktr("closedway")});
468 public void setType(String types) throws SAXException {
469 this.types = Arrays.asList(types.split(","));
470 for (String type : this.types) {
471 if(!allowedtypes.contains(type))
472 throw new SAXException(tr("Unknown type: {0}", type));
473 }
474 }
475
476 public static List<TaggingPreset> readAll(Reader in) throws SAXException {
477 XmlObjectParser parser = new XmlObjectParser();
478 parser.mapOnStart("item", TaggingPreset.class);
479 parser.mapOnStart("separator", TaggingPresetSeparator.class);
480 parser.mapBoth("group", TaggingPresetMenu.class);
481 parser.map("text", Text.class);
482 parser.map("link", Link.class);
483 parser.mapOnStart("optional", Optional.class);
484 parser.map("check", Check.class);
485 parser.map("combo", Combo.class);
486 parser.map("label", Label.class);
487 parser.map("space", Space.class);
488 parser.map("key", Key.class);
489 LinkedList<TaggingPreset> all = new LinkedList<TaggingPreset>();
490 TaggingPresetMenu lastmenu = null;
491 parser.start(in);
492 while(parser.hasNext()) {
493 Object o = parser.next();
494 if (o instanceof TaggingPresetMenu) {
495 TaggingPresetMenu tp = (TaggingPresetMenu) o;
496 if(tp == lastmenu)
497 lastmenu = tp.group;
498 else
499 {
500 tp.setDisplayName();
501 tp.group = lastmenu;
502 lastmenu = tp;
503 all.add(tp);
504 Main.toolbar.register(tp);
505
506 }
507 } else if (o instanceof TaggingPresetSeparator) {
508 TaggingPresetSeparator tp = (TaggingPresetSeparator) o;
509 tp.group = lastmenu;
510 all.add(tp);
511 } else if (o instanceof TaggingPreset) {
512 TaggingPreset tp = (TaggingPreset) o;
513 tp.group = lastmenu;
514 tp.setDisplayName();
515 all.add(tp);
516 Main.toolbar.register(tp);
517 } else
518 all.getLast().data.add((Item)o);
519 }
520 return all;
521 }
522
523 public static Collection<TaggingPreset> readFromPreferences() {
524 LinkedList<TaggingPreset> allPresets = new LinkedList<TaggingPreset>();
525 String allTaggingPresets = Main.pref.get("taggingpreset.sources");
526
527 if (Main.pref.getBoolean("taggingpreset.enable-defaults", true))
528 {
529 allTaggingPresets = "resource://presets/presets.xml"
530 + (allTaggingPresets != null ? ";"+allTaggingPresets : "");
531 }
532
533 for(String source : allTaggingPresets.split(";"))
534 {
535 try {
536 MirroredInputStream s = new MirroredInputStream(source);
537 InputStreamReader r;
538 try
539 {
540 r = new InputStreamReader(s, "UTF-8");
541 }
542 catch (UnsupportedEncodingException e)
543 {
544 r = new InputStreamReader(s);
545 }
546 allPresets.addAll(TaggingPreset.readAll(new BufferedReader(r)));
547 } catch (IOException e) {
548 e.printStackTrace();
549 JOptionPane.showMessageDialog(Main.parent, tr("Could not read tagging preset source: {0}",source));
550 } catch (SAXException e) {
551 e.printStackTrace();
552 JOptionPane.showMessageDialog(Main.parent, tr("Error parsing {0}: ", source)+e.getMessage());
553 }
554 }
555 return allPresets;
556 }
557
558 public JPanel createPanel(Collection<OsmPrimitive> selected) {
559 if (data == null)
560 return null;
561 JPanel p = new JPanel(new GridBagLayout());
562 LinkedList<Item> l = new LinkedList<Item>();
563 if(types != null)
564 {
565 JPanel pp = new JPanel();
566 for(String t : types)
567 {
568 JLabel la = new JLabel(ImageProvider.get("Mf_" + t));
569 la.setToolTipText(tr("Elements of type {0} are supported.", tr(t)));
570 pp.add(la);
571 }
572 p.add(pp, GBC.eol());
573 }
574
575 for (Item i : data)
576 {
577 if(i instanceof Link)
578 l.add(i);
579 else
580 i.addToPanel(p, selected);
581 }
582 for(Item link : l)
583 link.addToPanel(p, selected);
584 return p;
585 }
586
587 public void actionPerformed(ActionEvent e) {
588 Collection<OsmPrimitive> sel = createSelection(Main.ds.getSelected());
589 JPanel p = createPanel(sel);
590 if (p == null)
591 return;
592 int answer = JOptionPane.OK_OPTION;
593 if (p.getComponentCount() != 0) {
594 final JOptionPane optionPane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION){
595 @Override public void selectInitialValue() {
596 for (Item i : data) {
597 if (i.focus) {
598 i.requestFocusInWindow();
599 return;
600 }
601 }
602 }
603 };
604 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
605 if(sel.size() == 0)
606 title = tr("Nothing selected!");
607
608 optionPane.createDialog(Main.parent, title).setVisible(true);
609 Object answerObj = optionPane.getValue();
610 if (answerObj == null || answerObj == JOptionPane.UNINITIALIZED_VALUE ||
611 (answerObj instanceof Integer && (Integer)answerObj != JOptionPane.OK_OPTION))
612 answer = JOptionPane.CANCEL_OPTION;
613 }
614 if (sel.size() != 0 && answer == JOptionPane.OK_OPTION) {
615 Command cmd = createCommand(sel);
616 if (cmd != null)
617 Main.main.undoRedo.add(cmd);
618 }
619 Main.ds.setSelected(Main.ds.getSelected()); // force update
620 }
621
622 private Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) {
623 Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>();
624 for (OsmPrimitive osm : participants)
625 {
626 if (types != null)
627 {
628 if(osm instanceof Relation)
629 {
630 if(!types.contains("relation")) continue;
631 }
632 else if(osm instanceof Node)
633 {
634 if(!types.contains("node")) continue;
635 }
636 else if(osm instanceof Way)
637 {
638 if(!types.contains("way") &&
639 !(types.contains("closedway") && ((Way)osm).isClosed()))
640 continue;
641 }
642 }
643 sel.add(osm);
644 }
645 return sel;
646 }
647
648 private Command createCommand(Collection<OsmPrimitive> sel) {
649 List<Command> cmds = new LinkedList<Command>();
650 for (Item i : data)
651 i.addCommands(sel, cmds);
652 if (cmds.size() == 0)
653 return null;
654 else if (cmds.size() == 1)
655 return cmds.get(0);
656 else
657 return new SequenceCommand(tr("Change Properties"), cmds);
658 }
659}
Note: See TracBrowser for help on using the repository browser.