Subversion Repositories mkgmap

Rev

Rev 4369 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * 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.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;

import uk.me.parabola.mkgmap.osmstyle.eval.Op;
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<>();

        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)
                                throw new IllegalArgumentException("Invalid tagKey: " + tagKey);
                        if (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<>();
                // This is an index of all rules that start with EQUALS (A=B)
                Map<String, BitSet> tagVals = new HashMap<>();
               
                // This is an index of all rules by the tag name (A).
                Map<String, BitSet> tagnames = new HashMap<>();

                // remove unnecessary rules
                filterRules();
                buildInitialIndex(existKeys, tagVals, tagnames);
                findDependingRules(existKeys, tagVals, tagnames);
               
                // compress the index: create one hash map with one entry for each key
                for (Map.Entry<String, BitSet> entry : existKeys.entrySet()) {
                        Short skey = TagDict.getInstance().xlate(entry.getKey());
                        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);
                                TagHelper th = tagKeyMap.computeIfAbsent(key, k-> new TagHelper(null));
                                th.addTag(val, entry.getValue());
                        }
                }
                Optional<Short> minKey = tagKeyMap.keySet().stream().min(Short::compare);
                if (minKey.isPresent() && minKey.get() > 0) {
                        Optional<Short> maxKey = tagKeyMap.keySet().stream().max(Short::compare);
                        if (maxKey.isPresent()) {
                                tagKeyArray = new TagHelper[maxKey.get() + 1];
                                for (Map.Entry<Short, TagHelper> entry : tagKeyMap.entrySet()) {
                                        tagKeyArray[entry.getKey()] = entry.getValue();
                                }
                                tagKeyMap.clear();
                        }
                }

                inited = true;
        }

        private void buildInitialIndex(Map<String, BitSet> existKeys, Map<String, BitSet> tagVals,
                        Map<String, BitSet> tagnames) {
                for (int i = 0; i < ruleDetails.size(); i++) {
                        int ruleNumber = i;
                        RuleDetails rd = ruleDetails.get(i);
                        String keystring = rd.getKeystring();

                        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);
                                } else {
                                        assert false : "rule index: no = in keystring " + keystring;
                                }
                        }
                }
        }

        private void findDependingRules(Map<String, BitSet> existKeys, Map<String, BitSet> tagVals,
                        Map<String, BitSet> tagnames) {
                // find the additional rules which might be triggered as a result of actions changing tags.
                Map<Integer, BitSet> additionalRules = new LinkedHashMap<>();
                for (int i = 0; i < ruleDetails.size(); i++) {
                        int ruleNumber = i;
                        RuleDetails rd = ruleDetails.get(i);
                        final Set<String> changeableTags = rd.getChangingTags();
                        BitSet addedRules = new BitSet();
                        for (String s : changeableTags) {
                                int ind = s.indexOf('=');
                                if (ind >= 0) {
                                        BitSet set = tagVals.get(s);
                                        if (set != null) {
                                                addedRules.or(set);
                                        }

                                        // Exists rules can also be triggered, so add them too.
                                        String key = s.substring(0, ind);
                                        BitSet set1 = existKeys.get(key);
                                        if (set1 != null) {
                                                addedRules.or(set1);
                                        }
                                } else {
                                        BitSet set = tagnames.get(s);
                                        if (set != null)
                                                addedRules.or(set);
                                }
                        }
                        // Only rules after the current one can be affected
                        addedRules.clear(0, ruleNumber);
                        if (!addedRules.isEmpty()) {
                                additionalRules.put(ruleNumber, addedRules);
                        }
                }

                // now add all the additional rules to the existing sets
                for (Entry<Integer, BitSet> e : additionalRules.entrySet()) {
                        int ruleNumber = e.getKey();
                        BitSet addSet = e.getValue();
                        // 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)) {
                                for (Entry<String, BitSet> e2 : m.entrySet()) {
                                        BitSet bi = e2.getValue();
                                        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(addSet);
                                        }
                                }
                        }
                }
        }

        /**
         * Remove dead rules.
         * @param styleOptionTags
         */

        private void filterRules() {
                List<RuleDetails> filteredRules = new ArrayList<>(ruleDetails);
                Set<String> usedIfVars = new HashSet<>();
                for (RuleDetails rd : filteredRules) {
                        findIfVarUsage(rd.getRule(), usedIfVars);
                }
                removeUnused(filteredRules, usedIfVars);
                ruleDetails.clear();
                ruleDetails.addAll(filteredRules);
        }

        private static void removeUnused(List<RuleDetails> filteredRules, Set<String> usedIfVars) {
                if (usedIfVars.isEmpty())
                        return;
                filteredRules.removeIf(rd -> {
                        if (rd.getRule() instanceof ActionRule) {
                                ActionRule ar = (ActionRule) rd.getRule();
                                if (ar.toString().contains("set " + RuleFileReader.IF_PREFIX)) {
                                        for (String ifVars : usedIfVars) {
                                                if (ar.toString().contains("set " + ifVars)) {
                                                        return false;
                                                }
                                        }
                                        return true;
                                }
                        }
                        return false;
                });
        }

        private static void findIfVarUsage(Rule rule, Set<String> usedIfVars) {
                if (rule == null)
                        return;
                Op expr = null;
                if (rule instanceof ExpressionRule)
                        expr = ((ExpressionRule) rule).getOp();
                else if (rule instanceof ActionRule)
                        expr = ((ActionRule) rule).getOp();
                if (expr == null)
                        return;
                for (String usedTag : expr.getEvaluatedTagKeys()) {
                        if (usedTag.startsWith(RuleFileReader.IF_PREFIX))
                                usedIfVars.add(usedTag);
                }
        }

        private static void addNumberToMap(Map<String, BitSet> map, String key, int ruleNumber) {
                map.computeIfAbsent(key, k -> new BitSet()).set(ruleNumber);
        }

        public List<RuleDetails> getRuleDetails() {
                return ruleDetails;
        }
}