/* * Copyright (C) 2010. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ package uk.me.parabola.mkgmap.osmstyle; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import uk.me.parabola.mkgmap.reader.osm.Rule; import uk.me.parabola.mkgmap.reader.osm.TagDict; /** * An index to reduce the number of rules that have to be executed. * * <p>Only the first term (after rearrangement) of the rule is used in the * index. This will (currently) always be an EQUALS or EXISTS (A=B or A=*). * * <p>We look at the tags of the element and pick out all rules that have the * first term that matches the tag=value and tag=*, this is done by a single * lookup for each tag in the element (on average a low number such as 3). * * <p>So if the element had the one tag highway=primary and the rules were as * follows: * * <pre> * 1 surface=good { ... } * 2 highway=secondary { set fence=no; } * 3 highway=primary { set surface=good; } * 4 highway=* & abc=yes { } * 5 surface=good { } * 6 oneway=yes & highway=primary { } * </pre> * * We would select rules 3 and 4. No other rule can match initially. But there * is a further issue; if rule 3 matched it could set the surface tag. So we also * need to select rule 5. Rule 1 can not be matched because it occurs before * the rule that sets the tag, so it is not included. All this is precomputed * when the index is created, so we can still do a single lookup. * * <p>So the full set of rules that we need to match is 3, 4 and 5. * If rule 5 itself sets a tag, then we might have to add more rules and * so on. * * @author Steve Ratcliffe */ public class RuleIndex { private final List<RuleDetails> ruleDetails = new ArrayList<RuleDetails>(); private final Map<Short, TagHelper> tagKeyMap = new HashMap<>(); private TagHelper[] tagKeyArray = null; private boolean inited; private class TagHelper{ // This is an index of all rules that start with EXISTS (A=*) final BitSet exists; // This is an index of all rules that start with EQUALS (A=B) Map<String, BitSet> tagVals; public TagHelper(BitSet exits){ this.exists = exits; } public void addTag(String val, BitSet value) { if (tagVals == null) tagVals = new HashMap<>(); if (exists != null){ BitSet merged = new BitSet(); merged.or(exists); merged.or(value); tagVals.put(val, merged); } else tagVals.put(val, value); } public BitSet getBitSet(String tagVal) { if (tagVals != null){ BitSet set = tagVals.get(tagVal); if (set != null){ return (BitSet) set.clone(); } } if (exists != null) return (BitSet) exists.clone(); return new BitSet(); } } /** * Save the rule and maintains several lists related to it from the other * information that is supplied. * @param rd Contains 1) the key string which is the key into the index. * 2) the rule itself. 3) a list of the tags that might be changed by * this rule, should it be matched. */ public void addRuleToIndex(RuleDetails rd) { assert !inited; ruleDetails.add(rd); } /** * Get all the rules that have been added. This is used in the RuleSet * for looking up by number. * @return The rules as an array for quick lookup. */ public Rule[] getRules() { int len = ruleDetails.size(); Rule[] rules = new Rule[len]; for (int i = 0, ruleDetailsSize = ruleDetails.size(); i < ruleDetailsSize; i++) { RuleDetails rd = ruleDetails.get(i); rules[i] = rd.getRule(); } return rules; } /** * Get a list of rules that might be matched by this tag. * @param tagval The tag and its value eg highway=primary. * @return A BitSet of rules numbers. * If there are no rules then null will be returned. */ public BitSet getRulesForTag(short tagKey, String tagVal) { TagHelper th; if (tagKeyArray != null){ if (tagKey >= 0 & tagKey < tagKeyArray.length){ th = tagKeyArray[tagKey]; } else th = null; } else { th = tagKeyMap.get(tagKey); } if (th == null) return new BitSet(); return th.getBitSet(tagVal); } /** * Prepare the index for use. This involves merging in all the possible * rules that could be run as a result of actions changing tags. */ public void prepare() { if (inited) return; // This is an index of all rules that start with EXISTS (A=*) Map<String, BitSet> existKeys = new HashMap<String, BitSet>(); // This is an index of all rules that start with EQUALS (A=B) Map<String, BitSet> tagVals = new HashMap<String, BitSet>(); // This is an index of all rules by the tag name (A). Map<String, BitSet> tagnames = new HashMap<String, BitSet>(); // Maps a rule number to the tags that might be changed by that rule Map<Integer, List<String>> changeTags = new HashMap<Integer, List<String>>(); for (int i = 0; i < ruleDetails.size(); i++){ int ruleNumber = i; RuleDetails rd = ruleDetails.get(i); String keystring = rd.getKeystring(); Set<String> changeableTags = rd.getChangingTags(); if (keystring.endsWith("=*")) { String key = keystring.substring(0, keystring.length() - 2); addNumberToMap(existKeys, key, ruleNumber); addNumberToMap(tagnames, key, ruleNumber); } else { addNumberToMap(tagVals, keystring, ruleNumber); int ind = keystring.indexOf('='); if (ind >= 0) { String key = keystring.substring(0, ind); addNumberToMap(tagnames, key, ruleNumber); } } addChangables(changeTags, changeableTags, ruleNumber); } for (Map.Entry<Integer, List<String>> ent : changeTags.entrySet()) { int ruleNumber = ent.getKey(); List<String> changeTagList = ent.getValue(); // When we add new rules, we may, in turn get more changeable tags // which will force us to run again to find more rules that could // be executed. So save rules that we find here. Set<String> newChanged = new HashSet<String>(changeTagList); // we have to find all rules that might be now matched do { for (String s : new ArrayList<String>(newChanged)) { BitSet set; // If we know the value that could be set, then we can restrict to // rules that would match that value. Otherwise we look for any // rule using the tag, no matter what the value. int ind = s.indexOf('='); if (ind >= 0) { set = tagVals.get(s); // Exists rules can also be triggered, so add them too. String key = s.substring(0, ind); BitSet set1 = existKeys.get(key); if (set == null) set = set1; else if (set1 != null) set.or(set1); } else { set = tagnames.get(s); } if (set != null && !set.isEmpty()) { // create copy that can be safely modified BitSet tmp = new BitSet(); tmp.or(set); set = tmp; for (int i = set.nextSetBit(0); i >= 0; i = set.nextSetBit(i + 1)) { // Only rules after this one can be affected if (i > ruleNumber) { newChanged.addAll(ruleDetails.get(i).getChangingTags()); } else { set.clear(i); } } // Find every rule number set that contains the rule number that we // are examining and add all the newly found rules to each such set. for (Map<String, BitSet> m : Arrays.asList(existKeys, tagVals, tagnames)) { Collection<BitSet> bitSets = m.values(); for (BitSet bi : bitSets) { if (bi.get(ruleNumber)) { // contains the rule that we are looking at so we must // also add the rules in the set we found. bi.or(set); } } } } } newChanged.removeAll(changeTagList); changeTagList.addAll(newChanged); } while (!newChanged.isEmpty()); } // compress the index: create one hash map with one entry for each key int highestKey = 0; for (Map.Entry<String, BitSet> entry : existKeys.entrySet()){ Short skey = TagDict.getInstance().xlate(entry.getKey()); if (skey > highestKey) highestKey = skey; tagKeyMap.put(skey, new TagHelper(entry.getValue())); } for (Map.Entry<String, BitSet> entry : tagVals.entrySet()){ String keyString = entry.getKey(); int ind = keyString.indexOf('='); if (ind >= 0) { short key = TagDict.getInstance().xlate(keyString.substring(0, ind)); String val = keyString.substring(ind+1); if (key > highestKey) highestKey = key; TagHelper th = tagKeyMap.get(key); if (th == null){ th = new TagHelper(null); tagKeyMap.put(key, th); } th.addTag(val, entry.getValue()); } } if (highestKey > 0 && highestKey < 1024){ tagKeyArray = new TagHelper[highestKey+1]; for (Map.Entry<Short, TagHelper> entry : tagKeyMap.entrySet()){ tagKeyArray[entry.getKey()] = entry.getValue(); } tagKeyMap.clear(); } inited = true; } private static void addNumberToMap(Map<String, BitSet> map, String key, int ruleNumber) { BitSet set = map.get(key); if (set == null) { set = new BitSet(); map.put(key, set); } set.set(ruleNumber); } /** * For each rule number, we maintain a list of tags that might be * changed by that rule. * @param changeTags * @param changeableTags The tags that might be changed if the rule is * matched. * @param ruleNumber The rule number. */ private static void addChangables(Map<Integer, List<String>> changeTags, Set<String> changeableTags, int ruleNumber) { List<String> tags = changeTags.get(ruleNumber); if (tags == null) { tags = new ArrayList<String>(); changeTags.put(ruleNumber, tags); } tags.addAll(changeableTags); } public List<RuleDetails> getRuleDetails() { return ruleDetails; } }