/*
* 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 final static short executeFinalizeRulesTagKey = TagDict.getInstance().xlate("mkgmap:execute_finalize_rules");
private RuleIndex index = new RuleIndex();
private final Set<String> usedTags = new HashSet<String>();
@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 rules = index.getRulesForTag(tagEntry.getKey(), tagEntry.getValue());
if (rules != null && !rules.isEmpty() )
candidates.or(rules);
}
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){
if ("true".equals(el.getTag(executeFinalizeRulesTagKey))){
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();
//System.out.println("Merging used tags: "
// + getUsedTags().toString()
// + " + "
// + rs.getUsedTags());
addUsedTags(rs.usedTags);
//System.out.println("Result: " + getUsedTags().toString());
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<String, Op>();
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");
continue;
}
}
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;
}
if (finalizeRule != null){
if (finalizeRule.containsExpression(exp))
return true;
}
return false;
}
}