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