001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging; 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.Dimension; 010import java.awt.GridBagLayout; 011import java.awt.Insets; 012import java.awt.event.ActionEvent; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.EnumSet; 016import java.util.HashSet; 017import java.util.LinkedList; 018import java.util.List; 019import java.util.Map; 020 021import javax.swing.AbstractAction; 022import javax.swing.Action; 023import javax.swing.ImageIcon; 024import javax.swing.JLabel; 025import javax.swing.JPanel; 026import javax.swing.SwingUtilities; 027 028import org.openstreetmap.josm.Main; 029import org.openstreetmap.josm.actions.search.SearchCompiler; 030import org.openstreetmap.josm.actions.search.SearchCompiler.Match; 031import org.openstreetmap.josm.command.ChangePropertyCommand; 032import org.openstreetmap.josm.command.Command; 033import org.openstreetmap.josm.command.SequenceCommand; 034import org.openstreetmap.josm.data.osm.Node; 035import org.openstreetmap.josm.data.osm.OsmPrimitive; 036import org.openstreetmap.josm.data.osm.Relation; 037import org.openstreetmap.josm.data.osm.RelationMember; 038import org.openstreetmap.josm.data.osm.Tag; 039import org.openstreetmap.josm.data.osm.Way; 040import org.openstreetmap.josm.gui.ExtendedDialog; 041import org.openstreetmap.josm.gui.MapView; 042import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; 043import org.openstreetmap.josm.gui.layer.Layer; 044import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference; 045import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Link; 046import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role; 047import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Roles; 048import org.openstreetmap.josm.gui.util.GuiHelper; 049import org.openstreetmap.josm.tools.GBC; 050import org.openstreetmap.josm.tools.ImageProvider; 051import org.openstreetmap.josm.tools.Predicate; 052import org.openstreetmap.josm.tools.Utils; 053import org.openstreetmap.josm.tools.template_engine.ParseError; 054import org.openstreetmap.josm.tools.template_engine.TemplateEntry; 055import org.openstreetmap.josm.tools.template_engine.TemplateParser; 056import org.xml.sax.SAXException; 057 058/** 059 * This class read encapsulate one tagging preset. A class method can 060 * read in all predefined presets, either shipped with JOSM or that are 061 * in the config directory. 062 * 063 * It is also able to construct dialogs out of preset definitions. 064 * @since 294 065 */ 066public class TaggingPreset extends AbstractAction implements MapView.LayerChangeListener { 067 068 public static final int DIALOG_ANSWER_APPLY = 1; 069 public static final int DIALOG_ANSWER_NEW_RELATION = 2; 070 public static final int DIALOG_ANSWER_CANCEL = 3; 071 072 public TaggingPresetMenu group = null; 073 public String name; 074 public String name_context; 075 public String locale_name; 076 public final static String OPTIONAL_TOOLTIP_TEXT = "Optional tooltip text"; 077 078 /** 079 * The types as preparsed collection. 080 */ 081 public EnumSet<TaggingPresetType> types; 082 public List<TaggingPresetItem> data = new LinkedList<TaggingPresetItem>(); 083 public Roles roles; 084 public TemplateEntry nameTemplate; 085 public Match nameTemplateFilter; 086 087 /** 088 * Create an empty tagging preset. This will not have any items and 089 * will be an empty string as text. createPanel will return null. 090 * Use this as default item for "do not select anything". 091 */ 092 public TaggingPreset() { 093 MapView.addLayerChangeListener(this); 094 updateEnabledState(); 095 } 096 097 /** 098 * Change the display name without changing the toolbar value. 099 */ 100 public void setDisplayName() { 101 putValue(Action.NAME, getName()); 102 putValue("toolbar", "tagging_" + getRawName()); 103 putValue(OPTIONAL_TOOLTIP_TEXT, (group != null ? 104 tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) : 105 tr("Use preset ''{0}''", getLocaleName()))); 106 } 107 108 public String getLocaleName() { 109 if(locale_name == null) { 110 if(name_context != null) { 111 locale_name = trc(name_context, TaggingPresetItems.fixPresetString(name)); 112 } else { 113 locale_name = tr(TaggingPresetItems.fixPresetString(name)); 114 } 115 } 116 return locale_name; 117 } 118 119 public String getName() { 120 return group != null ? group.getName() + "/" + getLocaleName() : getLocaleName(); 121 } 122 public String getRawName() { 123 return group != null ? group.getRawName() + "/" + name : name; 124 } 125 126 /** 127 * Returns the preset icon. 128 * @return The preset icon, or {@code null} if none defined 129 * @since 6403 130 */ 131 public final ImageIcon getIcon() { 132 Object icon = getValue(Action.SMALL_ICON); 133 if (icon instanceof ImageIcon) { 134 return (ImageIcon) icon; 135 } 136 return null; 137 } 138 139 /** 140 * Called from the XML parser to set the icon. 141 * This task is performed in the background in order to speedup startup. 142 * 143 * FIXME for Java 1.6 - use 24x24 icons for LARGE_ICON_KEY (button bar) 144 * and the 16x16 icons for SMALL_ICON. 145 */ 146 public void setIcon(final String iconName) { 147 ImageProvider imgProv = new ImageProvider(iconName); 148 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null); 149 imgProv.setDirs(s); 150 imgProv.setId("presets"); 151 imgProv.setArchive(TaggingPresetReader.getZipIcons()); 152 imgProv.setOptional(true); 153 imgProv.setMaxWidth(16).setMaxHeight(16); 154 imgProv.getInBackground(new ImageProvider.ImageCallback() { 155 @Override 156 public void finished(final ImageIcon result) { 157 if (result != null) { 158 GuiHelper.runInEDT(new Runnable() { 159 @Override 160 public void run() { 161 putValue(Action.SMALL_ICON, result); 162 } 163 }); 164 } else { 165 Main.warn("Could not get presets icon " + iconName); 166 } 167 } 168 }); 169 } 170 171 /** 172 * Called from the XML parser to set the types this preset affects. 173 */ 174 public void setType(String types) throws SAXException { 175 this.types = TaggingPresetItems.getType(types); 176 } 177 178 public void setName_template(String pattern) throws SAXException { 179 try { 180 this.nameTemplate = new TemplateParser(pattern).parse(); 181 } catch (ParseError e) { 182 Main.error("Error while parsing " + pattern + ": " + e.getMessage()); 183 throw new SAXException(e); 184 } 185 } 186 187 public void setName_template_filter(String filter) throws SAXException { 188 try { 189 this.nameTemplateFilter = SearchCompiler.compile(filter, false, false); 190 } catch (org.openstreetmap.josm.actions.search.SearchCompiler.ParseError e) { 191 Main.error("Error while parsing" + filter + ": " + e.getMessage()); 192 throw new SAXException(e); 193 } 194 } 195 196 private static class PresetPanel extends JPanel { 197 boolean hasElements = false; 198 PresetPanel() 199 { 200 super(new GridBagLayout()); 201 } 202 } 203 204 public PresetPanel createPanel(Collection<OsmPrimitive> selected) { 205 if (data == null) 206 return null; 207 PresetPanel p = new PresetPanel(); 208 LinkedList<TaggingPresetItem> l = new LinkedList<TaggingPresetItem>(); 209 if(types != null){ 210 JPanel pp = new JPanel(); 211 for(TaggingPresetType t : types){ 212 JLabel la = new JLabel(ImageProvider.get(t.getIconName())); 213 la.setToolTipText(tr("Elements of type {0} are supported.", tr(t.getName()))); 214 pp.add(la); 215 } 216 p.add(pp, GBC.eol()); 217 } 218 219 JPanel items = new JPanel(new GridBagLayout()); 220 for (TaggingPresetItem i : data){ 221 if(i instanceof Link) { 222 l.add(i); 223 } else { 224 if(i.addToPanel(items, selected)) { 225 p.hasElements = true; 226 } 227 } 228 } 229 p.add(items, GBC.eol().fill()); 230 if (selected.isEmpty() && !supportsRelation()) { 231 GuiHelper.setEnabledRec(items, false); 232 } 233 234 for(TaggingPresetItem link : l) { 235 link.addToPanel(p, selected); 236 } 237 238 return p; 239 } 240 241 public boolean isShowable() 242 { 243 for(TaggingPresetItem i : data) 244 { 245 if(!(i instanceof TaggingPresetItems.Optional || i instanceof TaggingPresetItems.Space || i instanceof TaggingPresetItems.Key)) 246 return true; 247 } 248 return false; 249 } 250 251 public String suggestRoleForOsmPrimitive(OsmPrimitive osm) { 252 if (roles != null && osm != null) { 253 for (Role i : roles.roles) { 254 if (i.memberExpression != null && i.memberExpression.match(osm) 255 && (i.types == null || i.types.isEmpty() || i.types.contains(TaggingPresetType.forPrimitive(osm)) )) { 256 return i.key; 257 } 258 } 259 } 260 return null; 261 } 262 263 @Override 264 public void actionPerformed(ActionEvent e) { 265 if (Main.main == null) return; 266 if (Main.main.getCurrentDataSet() == null) return; 267 268 Collection<OsmPrimitive> sel = createSelection(Main.main.getCurrentDataSet().getSelected()); 269 int answer = showDialog(sel, supportsRelation()); 270 271 if (!sel.isEmpty() && answer == DIALOG_ANSWER_APPLY) { 272 Command cmd = createCommand(sel, getChangedTags()); 273 if (cmd != null) { 274 Main.main.undoRedo.add(cmd); 275 } 276 } else if (answer == DIALOG_ANSWER_NEW_RELATION) { 277 final Relation r = new Relation(); 278 final Collection<RelationMember> members = new HashSet<RelationMember>(); 279 for(Tag t : getChangedTags()) { 280 r.put(t.getKey(), t.getValue()); 281 } 282 for (OsmPrimitive osm : Main.main.getCurrentDataSet().getSelected()) { 283 String role = suggestRoleForOsmPrimitive(osm); 284 RelationMember rm = new RelationMember(role == null ? "" : role, osm); 285 r.addMember(rm); 286 members.add(rm); 287 } 288 SwingUtilities.invokeLater(new Runnable() { 289 @Override 290 public void run() { 291 RelationEditor.getEditor(Main.main.getEditLayer(), r, members).setVisible(true); 292 } 293 }); 294 } 295 Main.main.getCurrentDataSet().setSelected(Main.main.getCurrentDataSet().getSelected()); // force update 296 297 } 298 299 public int showDialog(Collection<OsmPrimitive> sel, final boolean showNewRelation) { 300 PresetPanel p = createPanel(sel); 301 if (p == null) 302 return DIALOG_ANSWER_CANCEL; 303 304 int answer = 1; 305 if (p.getComponentCount() != 0 && (sel.isEmpty() || p.hasElements)) { 306 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size()); 307 if(sel.isEmpty()) { 308 if(originalSelectionEmpty) { 309 title = tr("Nothing selected!"); 310 } else { 311 title = tr("Selection unsuitable!"); 312 } 313 } 314 315 class PresetDialog extends ExtendedDialog { 316 public PresetDialog(Component content, String title, ImageIcon icon, boolean disableApply) { 317 super(Main.parent, 318 title, 319 showNewRelation? 320 new String[] { tr("Apply Preset"), tr("New relation"), tr("Cancel") }: 321 new String[] { tr("Apply Preset"), tr("Cancel") }, 322 true); 323 if (icon != null) 324 setIconImage(icon.getImage()); 325 contentInsets = new Insets(10,5,0,5); 326 if (showNewRelation) { 327 setButtonIcons(new String[] {"ok.png", "dialogs/addrelation.png", "cancel.png" }); 328 } else { 329 setButtonIcons(new String[] {"ok.png", "cancel.png" }); 330 } 331 setContent(content); 332 setDefaultButton(1); 333 setupDialog(); 334 buttons.get(0).setEnabled(!disableApply); 335 buttons.get(0).setToolTipText(title); 336 // Prevent dialogs of being too narrow (fix #6261) 337 Dimension d = getSize(); 338 if (d.width < 350) { 339 d.width = 350; 340 setSize(d); 341 } 342 showDialog(); 343 } 344 } 345 346 answer = new PresetDialog(p, title, (ImageIcon) getValue(Action.SMALL_ICON), sel.isEmpty()).getValue(); 347 } 348 if (!showNewRelation && answer == 2) 349 return DIALOG_ANSWER_CANCEL; 350 else 351 return answer; 352 } 353 354 /** 355 * True whenever the original selection given into createSelection was empty 356 */ 357 private boolean originalSelectionEmpty = false; 358 359 /** 360 * Removes all unsuitable OsmPrimitives from the given list 361 * @param participants List of possible OsmPrimitives to tag 362 * @return Cleaned list with suitable OsmPrimitives only 363 */ 364 public Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) { 365 originalSelectionEmpty = participants.isEmpty(); 366 Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>(); 367 for (OsmPrimitive osm : participants) 368 { 369 if (types != null) 370 { 371 if(osm instanceof Relation) 372 { 373 if(!types.contains(TaggingPresetType.RELATION) && 374 !(types.contains(TaggingPresetType.CLOSEDWAY) && ((Relation)osm).isMultipolygon())) { 375 continue; 376 } 377 } 378 else if(osm instanceof Node) 379 { 380 if(!types.contains(TaggingPresetType.NODE)) { 381 continue; 382 } 383 } 384 else if(osm instanceof Way) 385 { 386 if(!types.contains(TaggingPresetType.WAY) && 387 !(types.contains(TaggingPresetType.CLOSEDWAY) && ((Way)osm).isClosed())) { 388 continue; 389 } 390 } 391 } 392 sel.add(osm); 393 } 394 return sel; 395 } 396 397 public List<Tag> getChangedTags() { 398 List<Tag> result = new ArrayList<Tag>(); 399 for (TaggingPresetItem i: data) { 400 i.addCommands(result); 401 } 402 return result; 403 } 404 405 public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) { 406 List<Command> cmds = new ArrayList<Command>(); 407 for (Tag tag: changedTags) { 408 cmds.add(new ChangePropertyCommand(sel, tag.getKey(), tag.getValue())); 409 } 410 411 if (cmds.size() == 0) 412 return null; 413 else if (cmds.size() == 1) 414 return cmds.get(0); 415 else 416 return new SequenceCommand(tr("Change Tags"), cmds); 417 } 418 419 private boolean supportsRelation() { 420 return types == null || types.contains(TaggingPresetType.RELATION); 421 } 422 423 protected void updateEnabledState() { 424 setEnabled(Main.main != null && Main.main.getCurrentDataSet() != null); 425 } 426 427 @Override 428 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 429 updateEnabledState(); 430 } 431 432 @Override 433 public void layerAdded(Layer newLayer) { 434 updateEnabledState(); 435 } 436 437 @Override 438 public void layerRemoved(Layer oldLayer) { 439 updateEnabledState(); 440 } 441 442 @Override 443 public String toString() { 444 return (types == null?"":types) + " " + name; 445 } 446 447 public boolean typeMatches(Collection<TaggingPresetType> t) { 448 return t == null || types == null || types.containsAll(t); 449 } 450 451 public boolean matches(Collection<TaggingPresetType> t, Map<String, String> tags, boolean onlyShowable) { 452 if (onlyShowable && !isShowable()) 453 return false; 454 else if (!typeMatches(t)) 455 return false; 456 boolean atLeastOnePositiveMatch = false; 457 for (TaggingPresetItem item : data) { 458 Boolean m = item.matches(tags); 459 if (m != null && !m) 460 return false; 461 else if (m != null) { 462 atLeastOnePositiveMatch = true; 463 } 464 } 465 return atLeastOnePositiveMatch; 466 } 467 468 public static Collection<TaggingPreset> getMatchingPresets(final Collection<TaggingPresetType> t, final Map<String, String> tags, final boolean onlyShowable) { 469 return Utils.filter(TaggingPresetPreference.taggingPresets, new Predicate<TaggingPreset>() { 470 @Override 471 public boolean evaluate(TaggingPreset object) { 472 return object.matches(t, tags, onlyShowable); 473 } 474 }); 475 } 476}