Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2008 Steve Ratcliffe
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License 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.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: 08-Nov-2008
 */

package uk.me.parabola.mkgmap.osmstyle;

import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;

import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.osmstyle.eval.AbstractBinaryOp;
import uk.me.parabola.mkgmap.osmstyle.eval.AbstractOp;
import uk.me.parabola.mkgmap.osmstyle.eval.LinkedBinaryOp;
import uk.me.parabola.mkgmap.osmstyle.eval.LinkedOp;
import uk.me.parabola.mkgmap.osmstyle.eval.Op;
import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.reader.osm.Rule;
import uk.me.parabola.mkgmap.reader.osm.TagDict;
import uk.me.parabola.mkgmap.reader.osm.TypeResult;
import uk.me.parabola.mkgmap.reader.osm.WatchableTypeResult;

/**
 * A list of rules and the logic to select the correct one.
 *
 * A separate {@link RuleIndex} class is used to speed access to the rule list.
 *
 * @author Steve Ratcliffe
 */

public class RuleSet implements Rule, Iterable<Rule> {
        private static final Logger log = Logger.getLogger(RuleSet.class);
        private Rule[] rules;
        private Rule finalizeRule;

        // identifies cached values
        int cacheId;
        boolean compiled = false;

        private static final short TKM_EXECUTE_FINALIZE_RULES = TagDict.getInstance().xlate("mkgmap:execute_finalize_rules");

        private RuleIndex index = new RuleIndex();
        private final Set<String> usedTags = new HashSet<>();
       
        @Override
        public void resolveType(Element el, TypeResult result) {
                cacheId = resolveType(cacheId, el, result);
        }
       
       
        /**
         * Resolve the type for this element by running the rules in order.
         *
         * This is a very performance critical part of the style system as parts
         * of the code are run for every tag in the input file.
         *
         * @param el The element as read from an OSM xml file in 'tag' format.
         * @param result A GType describing the Garmin type of the first rule that
         * matches is returned here.  If continue types are used then more than
         * one type may be saved here.  If there are no matches then nothing will
         * be saved.
         */

        public int resolveType(int cacheId, Element el, TypeResult result) {
                WatchableTypeResult a = new WatchableTypeResult(result);
                if (!compiled || cacheId == Integer.MAX_VALUE)
                        compile();
                // new element, invalidate all caches
                cacheId++;
               
                // Get all the rules that could match from the index.  
                BitSet candidates = new BitSet();
                for (Entry<Short, String> tagEntry : el.getFastTagEntryIterator()) {
                        BitSet bsRules = index.getRulesForTag(tagEntry.getKey(), tagEntry.getValue());
                        if (bsRules != null && !bsRules.isEmpty() )
                                candidates.or(bsRules);
                }
                Rule lastRule = null;
                for (int i = candidates.nextSetBit(0); i >= 0; i = candidates.nextSetBit(i + 1)) {
                        a.reset();
                        lastRule = rules[i];
                        cacheId = lastRule.resolveType(cacheId, el, a);
                        if (a.isResolved())
                                return cacheId;
                }
                if (lastRule != null && lastRule.getFinalizeRule() != null
                                && "true".equals(el.getTag(TKM_EXECUTE_FINALIZE_RULES))) {
                        cacheId = lastRule.getFinalizeRule().resolveType(cacheId, el, a);
                }
                return cacheId;
        }

        public Iterator<Rule> iterator() {
                if (rules == null)
                        prepare();
                return Arrays.asList(rules).iterator();
        }

        /**
         * Add a rule to this rule set.
         * @param keystring The string form of the first term of the rule.  It will
         * be A=B or A=*.  (In the future we may allow other forms).
         * @param rule The actual rule.
         * @param changeableTags The tags that may be changed by the rule.  This
         * will be either a plain tag name A, or with a value A=B.
         */

        public void add(String keystring, Rule rule, Set<String> changeableTags) {
                compiled = false;
                index.addRuleToIndex(new RuleDetails(keystring, rule, changeableTags));
        }

        /**
         * Add all rules from the given rule set to this one.
         * @param rs The other rule set.
         */

        public void addAll(RuleSet rs) {
                for (RuleDetails rd : rs.index.getRuleDetails())
                        add(rd.getKeystring(), rd.getRule(), rd.getChangingTags());
        }

        /**
         * Format the rule set.  Warning: this doesn't produce a valid input
         * rule file.
         */

        public String toString() {
                StringBuilder sb = new StringBuilder();
                for (Rule rule : rules) {
                        sb.append(rule.toString());
                }
                return sb.toString();
        }

        /**
         * Merge the two rulesets together so that they appear to be one.
         * @param rs The other rule set, it will have lower priority, that is the
         * rules will be tried after the rules of this ruleset.
         */

        public void merge(RuleSet rs) {
                // We have to basically rebuild the index and reset the rule list.
                RuleIndex newIndex = new RuleIndex();

                for (RuleDetails rd : index.getRuleDetails())
                        newIndex.addRuleToIndex(rd);

                for (RuleDetails rd : rs.index.getRuleDetails())
                        newIndex.addRuleToIndex(rd);

                index = newIndex;
                rules = newIndex.getRules();
                addUsedTags(rs.usedTags);
                compiled = false;
        }

        /**
         * Prepare this rule set for use.  The index is built and and the rules
         * are saved to an array for fast access.
         */

        public void prepare() {
                index.prepare();
                rules = index.getRules();
                compile();
        }

        public Set<String> getUsedTags() {
                return usedTags;
        }

        public void addUsedTags(Collection<String> usedTags) {
                this.usedTags.addAll(usedTags);
        }

        /**
         * Compile the rules and reset caches. Detect common sub-expressions and
         * make sure that all rules use the same instance of these common
         * sub-expressions.
         */

        private void compile() {
                HashMap<String, Op> tests = new HashMap<>();

                for (Rule rule : rules) {
                        Op op;
                        if (rule instanceof ExpressionRule)
                                op = ((ExpressionRule) rule).getOp();
                        else if (rule instanceof ActionRule)
                                op = ((ActionRule) rule).getOp();
                        else {
                                log.error("unexpected rule instance");
                                continue;
                        }
                        if (op instanceof AbstractBinaryOp) {
                                AbstractBinaryOp binOp = (AbstractBinaryOp) op;
                                binOp.setFirst(compileOp(tests, binOp.getFirst()));
                                binOp.setSecond(compileOp(tests, binOp.getSecond()));
                                op = compileOp(tests, binOp);
                        } else if (op instanceof AbstractOp) {
                                op = compileOp(tests, op);
                        } else if (op instanceof LinkedBinaryOp) {
                                ((LinkedBinaryOp) op).setFirst(compileOp(tests, ((LinkedBinaryOp) op).getFirst()));
                                ((LinkedBinaryOp) op).setSecond(compileOp(tests, ((LinkedBinaryOp) op).getSecond()));
                        } else if (op instanceof LinkedOp) {
                                Op wrappedOp = compileOp(tests, ((LinkedOp) op).getFirst());
                                op.setFirst(wrappedOp);
                        } else {
                                log.error("unexpected op instance");
                                continue;
                        }
                        if (rule instanceof ExpressionRule)
                                ((ExpressionRule) rule).setOp(op);
                        else if (rule instanceof ActionRule)
                                ((ActionRule) rule).setOp(op);
                        else {
                                log.error("unexpected rule instance");
                        }
                }
                cacheId = 0;
                compiled = true;
        }
       
        private Op compileOp(HashMap<String, Op> tests, Op op){
                if (op instanceof AbstractBinaryOp){
                        AbstractBinaryOp binOp = (AbstractBinaryOp) op;
                        binOp.setFirst(compileOp(tests, binOp.getFirst()));
                        binOp.setSecond(compileOp(tests, binOp.getSecond()));
                }
                if (op instanceof LinkedOp){
                        // LinkedOp is referenced by other OPs, don't replace it
                        return op;
                }
                String test = op.toString();
                Op commonOp = tests.get(test);
                if (commonOp == null){
                        if (op instanceof AbstractOp)
                                ((AbstractOp)op).resetCache();
                        tests.put(test, op);
                        commonOp = op;
                }
               
                return commonOp;
        }
       
        public void setFinalizeRule(Rule finalizeRule) {
                if (rules == null) {
                        // this method must be called after prepare() is called so
                        // that we have rules to which the finalize rules can be applied
                        throw new IllegalStateException("First call prepare() before setting the finalize rules");
                }
                for (Rule rule : rules)
                        rule.setFinalizeRule(finalizeRule);
               
                compiled = false;
                this.finalizeRule = finalizeRule;  
        }

        @Override
        public Rule getFinalizeRule() {
                return finalizeRule;
        }

        @Override
        public void printStats(String header) {
                if (rules == null)
                  return;
                for (Rule rule : rules){
                        rule.printStats(header);
                }
                if (finalizeRule != null)
                        finalizeRule.printStats(header);
        }
       
        @Override
        public boolean containsExpression(String exp) {
                if (rules == null) {
                        // this method must be called after prepare() is called so
                        // that we have rules to which the finalize rules can be applied
                        throw new IllegalStateException("First call prepare() before setting the finalize rules");
                }
                for (Rule rule : rules) {
                        if (rule.containsExpression(exp))
                                return true;
                }
                return finalizeRule != null && finalizeRule.containsExpression(exp);
        }

        public BitSet getRules(Element el) {
                if (!compiled || cacheId == Integer.MAX_VALUE)
                        compile();
                // new element, invalidate all caches
                cacheId++;

                // Get all the rules that could match from the index.
                BitSet candidates = new BitSet();
                for (Entry<Short, String> tagEntry : el.getFastTagEntryIterator()) {
                        BitSet bsRules = index.getRulesForTag(tagEntry.getKey(), tagEntry.getValue());
                        if (bsRules != null && !bsRules.isEmpty())
                                candidates.or(bsRules);
                }
                return candidates;
        }
       
        @Override
        public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) {
                if (rules == null)
                        return;
                for (Rule rule: rules)
                        rule.augmentWith(elementSaver);
        }
}