001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.ac; 003 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.Set; 013 014import org.openstreetmap.josm.Main; 015import org.openstreetmap.josm.data.osm.DataSet; 016import org.openstreetmap.josm.data.osm.OsmPrimitive; 017import org.openstreetmap.josm.data.osm.Relation; 018import org.openstreetmap.josm.data.osm.RelationMember; 019import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 020import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 021import org.openstreetmap.josm.data.osm.event.DataSetListener; 022import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 023import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 024import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 025import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 026import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 027import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 028import org.openstreetmap.josm.gui.tagging.TaggingPreset; 029import org.openstreetmap.josm.gui.tagging.TaggingPresetItem; 030import org.openstreetmap.josm.gui.tagging.TaggingPresetItems; 031import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role; 032import org.openstreetmap.josm.tools.CheckParameterUtil; 033import org.openstreetmap.josm.tools.MultiMap; 034import org.openstreetmap.josm.tools.Utils; 035 036/** 037 * AutoCompletionManager holds a cache of keys with a list of 038 * possible auto completion values for each key. 039 * 040 * Each DataSet is assigned one AutoCompletionManager instance such that 041 * <ol> 042 * <li>any key used in a tag in the data set is part of the key list in the cache</li> 043 * <li>any value used in a tag for a specific key is part of the autocompletion list of 044 * this key</li> 045 * </ol> 046 * 047 * Building up auto completion lists should not 048 * slow down tabbing from input field to input field. Looping through the complete 049 * data set in order to build up the auto completion list for a specific input 050 * field is not efficient enough, hence this cache. 051 * 052 * TODO: respect the relation type for member role autocompletion 053 */ 054public class AutoCompletionManager implements DataSetListener { 055 056 /** If the dirty flag is set true, a rebuild is necessary. */ 057 protected boolean dirty; 058 /** The data set that is managed */ 059 protected DataSet ds; 060 061 /** 062 * the cached tags given by a tag key and a list of values for this tag 063 * only accessed by getTagCache(), rebuild() and cachePrimitiveTags() 064 * use getTagCache() accessor 065 */ 066 protected MultiMap<String, String> tagCache; 067 /** 068 * the same as tagCache but for the preset keys and values 069 * can be accessed directly 070 */ 071 protected static final MultiMap<String, String> presetTagCache = new MultiMap<>(); 072 /** 073 * the cached list of member roles 074 * only accessed by getRoleCache(), rebuild() and cacheRelationMemberRoles() 075 * use getRoleCache() accessor 076 */ 077 protected Set<String> roleCache; 078 /** 079 * the same as roleCache but for the preset roles 080 * can be accessed directly 081 */ 082 protected static final Set<String> presetRoleCache = new HashSet<>(); 083 084 public AutoCompletionManager(DataSet ds) { 085 this.ds = ds; 086 dirty = true; 087 } 088 089 protected MultiMap<String, String> getTagCache() { 090 if (dirty) { 091 rebuild(); 092 dirty = false; 093 } 094 return tagCache; 095 } 096 097 protected Set<String> getRoleCache() { 098 if (dirty) { 099 rebuild(); 100 dirty = false; 101 } 102 return roleCache; 103 } 104 105 /** 106 * initializes the cache from the primitives in the dataset 107 * 108 */ 109 protected void rebuild() { 110 tagCache = new MultiMap<>(); 111 roleCache = new HashSet<>(); 112 cachePrimitives(ds.allNonDeletedCompletePrimitives()); 113 } 114 115 protected void cachePrimitives(Collection<? extends OsmPrimitive> primitives) { 116 for (OsmPrimitive primitive : primitives) { 117 cachePrimitiveTags(primitive); 118 if (primitive instanceof Relation) { 119 cacheRelationMemberRoles((Relation) primitive); 120 } 121 } 122 } 123 124 /** 125 * make sure, the keys and values of all tags held by primitive are 126 * in the auto completion cache 127 * 128 * @param primitive an OSM primitive 129 */ 130 protected void cachePrimitiveTags(OsmPrimitive primitive) { 131 for (String key: primitive.keySet()) { 132 String value = primitive.get(key); 133 tagCache.put(key, value); 134 } 135 } 136 137 /** 138 * Caches all member roles of the relation <code>relation</code> 139 * 140 * @param relation the relation 141 */ 142 protected void cacheRelationMemberRoles(Relation relation){ 143 for (RelationMember m: relation.getMembers()) { 144 if (m.hasRole()) { 145 roleCache.add(m.getRole()); 146 } 147 } 148 } 149 150 /** 151 * Initialize the cache for presets. This is done only once. 152 */ 153 public static void cachePresets(Collection<TaggingPreset> presets) { 154 for (final TaggingPreset p : presets) { 155 for (TaggingPresetItem item : p.data) { 156 if (item instanceof TaggingPresetItems.KeyedItem) { 157 TaggingPresetItems.KeyedItem ki = (TaggingPresetItems.KeyedItem) item; 158 if (ki.key != null && ki.getValues() != null) { 159 try { 160 presetTagCache.putAll(ki.key, ki.getValues()); 161 } catch (NullPointerException e) { 162 Main.error(p+": Unable to cache "+ki); 163 } 164 } 165 } else if (item instanceof TaggingPresetItems.Roles) { 166 TaggingPresetItems.Roles r = (TaggingPresetItems.Roles) item; 167 for (TaggingPresetItems.Role i : r.roles) { 168 if (i.key != null) { 169 presetRoleCache.add(i.key); 170 } 171 } 172 } 173 } 174 } 175 } 176 177 /** 178 * replies the keys held by the cache 179 * 180 * @return the list of keys held by the cache 181 */ 182 protected List<String> getDataKeys() { 183 return new ArrayList<>(getTagCache().keySet()); 184 } 185 186 protected List<String> getPresetKeys() { 187 return new ArrayList<>(presetTagCache.keySet()); 188 } 189 190 /** 191 * replies the auto completion values allowed for a specific key. Replies 192 * an empty list if key is null or if key is not in {@link #getKeys()}. 193 * 194 * @param key 195 * @return the list of auto completion values 196 */ 197 protected List<String> getDataValues(String key) { 198 return new ArrayList<>(getTagCache().getValues(key)); 199 } 200 201 protected static List<String> getPresetValues(String key) { 202 return new ArrayList<>(presetTagCache.getValues(key)); 203 } 204 205 /** 206 * Replies the list of member roles 207 * 208 * @return the list of member roles 209 */ 210 public List<String> getMemberRoles() { 211 return new ArrayList<>(getRoleCache()); 212 } 213 214 /** 215 * Populates the {@link AutoCompletionList} with the currently cached 216 * member roles. 217 * 218 * @param list the list to populate 219 */ 220 public void populateWithMemberRoles(AutoCompletionList list) { 221 list.add(presetRoleCache, AutoCompletionItemPriority.IS_IN_STANDARD); 222 list.add(getRoleCache(), AutoCompletionItemPriority.IS_IN_DATASET); 223 } 224 225 /** 226 * Populates the {@link AutoCompletionList} with the roles used in this relation 227 * plus the ones defined in its applicable presets, if any. If the relation type is unknown, 228 * then all the roles known globally will be added, as in {@link #populateWithMemberRoles(AutoCompletionList)}. 229 * 230 * @param list the list to populate 231 * @param r the relation to get roles from 232 * @throws IllegalArgumentException if list is null 233 * @since 7556 234 */ 235 public void populateWithMemberRoles(AutoCompletionList list, Relation r) { 236 CheckParameterUtil.ensureParameterNotNull(list, "list"); 237 Collection<TaggingPreset> presets = r != null ? TaggingPreset.getMatchingPresets(null, r.getKeys(), false) : null; 238 if (r != null && presets != null && !presets.isEmpty()) { 239 for (TaggingPreset tp : presets) { 240 if (tp.roles != null) { 241 list.add(Utils.transform(tp.roles.roles, new Utils.Function<Role, String>() { 242 public String apply(Role x) { 243 return x.key; 244 } 245 }), AutoCompletionItemPriority.IS_IN_STANDARD); 246 } 247 } 248 list.add(r.getMemberRoles(), AutoCompletionItemPriority.IS_IN_DATASET); 249 } else { 250 populateWithMemberRoles(list); 251 } 252 } 253 254 /** 255 * Populates the an {@link AutoCompletionList} with the currently cached tag keys 256 * 257 * @param list the list to populate 258 */ 259 public void populateWithKeys(AutoCompletionList list) { 260 list.add(getPresetKeys(), AutoCompletionItemPriority.IS_IN_STANDARD); 261 list.add(new AutoCompletionListItem("source", AutoCompletionItemPriority.IS_IN_STANDARD)); 262 list.add(getDataKeys(), AutoCompletionItemPriority.IS_IN_DATASET); 263 } 264 265 /** 266 * Populates the an {@link AutoCompletionList} with the currently cached 267 * values for a tag 268 * 269 * @param list the list to populate 270 * @param key the tag key 271 */ 272 public void populateWithTagValues(AutoCompletionList list, String key) { 273 populateWithTagValues(list, Arrays.asList(key)); 274 } 275 276 /** 277 * Populates the an {@link AutoCompletionList} with the currently cached 278 * values for some given tags 279 * 280 * @param list the list to populate 281 * @param keys the tag keys 282 */ 283 public void populateWithTagValues(AutoCompletionList list, List<String> keys) { 284 for (String key : keys) { 285 list.add(getPresetValues(key), AutoCompletionItemPriority.IS_IN_STANDARD); 286 list.add(getDataValues(key), AutoCompletionItemPriority.IS_IN_DATASET); 287 } 288 } 289 290 /** 291 * Returns the currently cached tag keys. 292 * @return a list of tag keys 293 */ 294 public List<AutoCompletionListItem> getKeys() { 295 AutoCompletionList list = new AutoCompletionList(); 296 populateWithKeys(list); 297 return list.getList(); 298 } 299 300 /** 301 * Returns the currently cached tag values for a given tag key. 302 * @param key the tag key 303 * @return a list of tag values 304 */ 305 public List<AutoCompletionListItem> getValues(String key) { 306 return getValues(Arrays.asList(key)); 307 } 308 309 /** 310 * Returns the currently cached tag values for a given list of tag keys. 311 * @param keys the tag keys 312 * @return a list of tag values 313 */ 314 public List<AutoCompletionListItem> getValues(List<String> keys) { 315 AutoCompletionList list = new AutoCompletionList(); 316 populateWithTagValues(list, keys); 317 return list.getList(); 318 } 319 320 /********************************************************* 321 * Implementation of the DataSetListener interface 322 * 323 **/ 324 325 @Override 326 public void primitivesAdded(PrimitivesAddedEvent event) { 327 if (dirty) 328 return; 329 cachePrimitives(event.getPrimitives()); 330 } 331 332 @Override 333 public void primitivesRemoved(PrimitivesRemovedEvent event) { 334 dirty = true; 335 } 336 337 @Override 338 public void tagsChanged(TagsChangedEvent event) { 339 if (dirty) 340 return; 341 Map<String, String> newKeys = event.getPrimitive().getKeys(); 342 Map<String, String> oldKeys = event.getOriginalKeys(); 343 344 if (!newKeys.keySet().containsAll(oldKeys.keySet())) { 345 // Some keys removed, might be the last instance of key, rebuild necessary 346 dirty = true; 347 } else { 348 for (Entry<String, String> oldEntry: oldKeys.entrySet()) { 349 if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) { 350 // Value changed, might be last instance of value, rebuild necessary 351 dirty = true; 352 return; 353 } 354 } 355 cachePrimitives(Collections.singleton(event.getPrimitive())); 356 } 357 } 358 359 @Override 360 public void nodeMoved(NodeMovedEvent event) {/* ignored */} 361 362 @Override 363 public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */} 364 365 @Override 366 public void relationMembersChanged(RelationMembersChangedEvent event) { 367 dirty = true; // TODO: not necessary to rebuid if a member is added 368 } 369 370 @Override 371 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */} 372 373 @Override 374 public void dataChanged(DataChangedEvent event) { 375 dirty = true; 376 } 377}