/*
* Copyright 2010, 2011, 2012 mapsforge.org
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.android.maps.rendertheme;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Pattern;
import org.mapsforge.android.maps.rendertheme.renderinstruction.RenderInstruction;
import org.mapsforge.core.model.Tag;
import org.xml.sax.Attributes;
abstract class Rule {
private static final Map<List<String>, AttributeMatcher> MATCHERS_CACHE_KEY = new HashMap<>();
private static final Map<List<String>, AttributeMatcher> MATCHERS_CACHE_VALUE = new HashMap<>();
private static final Pattern SPLIT_PATTERN = Pattern.compile("\\|");
private static final String STRING_NEGATION = "~";
private static final String STRING_WILDCARD = "*";
private static final String UNKNOWN_ENUM_VALUE = "unknown enum value: ";
private static Rule createRule(Stack<Rule> ruleStack, Element element, String keys, String values, Closed closed,
byte zoomMin, byte zoomMax) {
ElementMatcher elementMatcher = getElementMatcher(element);
ClosedMatcher closedMatcher = getClosedMatcher(closed);
List<String> keyList = new ArrayList<>(Arrays.asList(SPLIT_PATTERN.split(keys)));
List<String> valueList = new ArrayList<>(Arrays.asList(SPLIT_PATTERN.split(values)));
elementMatcher = RuleOptimizer.optimize(elementMatcher, ruleStack);
closedMatcher = RuleOptimizer.optimize(closedMatcher, ruleStack);
if (valueList.remove(STRING_NEGATION)) {
AttributeMatcher attributeMatcher = new NegativeMatcher(keyList, valueList);
return new NegativeRule(elementMatcher, closedMatcher, zoomMin, zoomMax, attributeMatcher);
}
AttributeMatcher keyMatcher = getKeyMatcher(keyList);
AttributeMatcher valueMatcher = getValueMatcher(valueList);
keyMatcher = RuleOptimizer.optimize(keyMatcher, ruleStack);
valueMatcher = RuleOptimizer.optimize(valueMatcher, ruleStack);
return new PositiveRule(elementMatcher, closedMatcher, zoomMin, zoomMax, keyMatcher, valueMatcher);
}
private static ClosedMatcher getClosedMatcher(Closed closed) {
switch (closed) {
case YES:
return ClosedWayMatcher.getInstance();
case NO:
return LinearWayMatcher.getInstance();
case ANY:
return AnyMatcher.getInstance();
}
throw new IllegalArgumentException(UNKNOWN_ENUM_VALUE + closed);
}
private static ElementMatcher getElementMatcher(Element element) {
switch (element) {
case NODE:
return ElementNodeMatcher.getInstance();
case WAY:
return ElementWayMatcher.getInstance();
case ANY:
return AnyMatcher.getInstance();
}
throw new IllegalArgumentException(UNKNOWN_ENUM_VALUE + element);
}
private static AttributeMatcher getKeyMatcher(List<String> keyList) {
if (STRING_WILDCARD.equals(keyList.get(0))) {
return AnyMatcher.getInstance();
}
AttributeMatcher attributeMatcher = MATCHERS_CACHE_KEY.get(keyList);
if (attributeMatcher == null) {
if (keyList.size() == 1) {
attributeMatcher = new SingleKeyMatcher(keyList.get(0));
} else {
attributeMatcher = new MultiKeyMatcher(keyList);
}
MATCHERS_CACHE_KEY.put(keyList, attributeMatcher);
}
return attributeMatcher;
}
private static AttributeMatcher getValueMatcher(List<String> valueList) {
if (STRING_WILDCARD.equals(valueList.get(0))) {
return AnyMatcher.getInstance();
}
AttributeMatcher attributeMatcher = MATCHERS_CACHE_VALUE.get(valueList);
if (attributeMatcher == null) {
if (valueList.size() == 1) {
attributeMatcher = new SingleValueMatcher(valueList.get(0));
} else {
attributeMatcher = new MultiValueMatcher(valueList);
}
MATCHERS_CACHE_VALUE.put(valueList, attributeMatcher);
}
return attributeMatcher;
}
private static void validate(String elementName, Element element, String keys, String values, byte zoomMin,
byte zoomMax) {
if (element == null) {
throw new IllegalArgumentException("missing attribute e for element: " + elementName);
} else if (keys == null) {
throw new IllegalArgumentException("missing attribute k for element: " + elementName);
} else if (values == null) {
throw new IllegalArgumentException("missing attribute v for element: " + elementName);
} else if (zoomMin < 0) {
throw new IllegalArgumentException("zoom-min must not be negative: " + zoomMin);
} else if (zoomMax < 0) {
throw new IllegalArgumentException("zoom-max must not be negative: " + zoomMax);
} else if (zoomMin > zoomMax) {
throw new IllegalArgumentException("zoom-min must be less or equal zoom-max: " + zoomMin);
}
}
static Rule create(String elementName, Attributes attributes, Stack<Rule> ruleStack) {
Element element = null;
String keys = null;
String values = null;
Closed closed = Closed.ANY;
byte zoomMin = 0;
byte zoomMax = Byte.MAX_VALUE;
for (int i = 0; i < attributes.getLength(); ++i) {
String name = attributes.getLocalName(i);
String value = attributes.getValue(i);
if ("e".equals(name)) {
element = Element.valueOf(value.toUpperCase(Locale.ENGLISH));
} else if ("k".equals(name)) {
keys = value;
} else if ("v".equals(name)) {
values = value;
} else if ("closed".equals(name)) {
closed = Closed.valueOf(value.toUpperCase(Locale.ENGLISH));
} else if ("zoom-min".equals(name)) {
zoomMin = Byte.parseByte(value);
} else if ("zoom-max".equals(name)) {
zoomMax = Byte.parseByte(value);
} else {
RenderThemeHandler.logUnknownAttribute(elementName, name, value, i);
}
}
validate(elementName, element, keys, values, zoomMin, zoomMax);
return createRule(ruleStack, element, keys, values, closed, zoomMin, zoomMax);
}
private final ArrayList<RenderInstruction> renderInstructions;
private final ArrayList<Rule> subRules;
final ClosedMatcher closedMatcher;
final ElementMatcher elementMatcher;
final byte zoomMax;
final byte zoomMin;
Rule(ElementMatcher elementMatcher, ClosedMatcher closedMatcher, byte zoomMin, byte zoomMax) {
this.elementMatcher = elementMatcher;
this.closedMatcher = closedMatcher;
this.zoomMin = zoomMin;
this.zoomMax = zoomMax;
this.renderInstructions = new ArrayList<>(4);
this.subRules = new ArrayList<>(4);
}
void addRenderingInstruction(RenderInstruction renderInstruction) {
this.renderInstructions.add(renderInstruction);
}
void addSubRule(Rule rule) {
this.subRules.add(rule);
}
abstract boolean matchesNode(List<Tag> tags, byte zoomLevel);
abstract boolean matchesWay(List<Tag> tags, byte zoomLevel, Closed closed);
void matchNode(RenderCallback renderCallback, List<Tag> tags, byte zoomLevel) {
if (matchesNode(tags, zoomLevel)) {
for (int i = 0, n = this.renderInstructions.size(); i < n; ++i) {
this.renderInstructions.get(i).renderNode(renderCallback, tags);
}
for (int i = 0, n = this.subRules.size(); i < n; ++i) {
this.subRules.get(i).matchNode(renderCallback, tags, zoomLevel);
}
}
}
void matchWay(RenderCallback renderCallback, List<Tag> tags, byte zoomLevel, Closed closed,
List<RenderInstruction> matchingList) {
if (matchesWay(tags, zoomLevel, closed)) {
for (int i = 0, n = this.renderInstructions.size(); i < n; ++i) {
this.renderInstructions.get(i).renderWay(renderCallback, tags);
matchingList.add(this.renderInstructions.get(i));
}
for (int i = 0, n = this.subRules.size(); i < n; ++i) {
this.subRules.get(i).matchWay(renderCallback, tags, zoomLevel, closed, matchingList);
}
}
}
void onComplete() {
MATCHERS_CACHE_KEY.clear();
MATCHERS_CACHE_VALUE.clear();
this.renderInstructions.trimToSize();
this.subRules.trimToSize();
for (int i = 0, n = this.subRules.size(); i < n; ++i) {
this.subRules.get(i).onComplete();
}
}
void onDestroy() {
for (int i = 0, n = this.renderInstructions.size(); i < n; ++i) {
this.renderInstructions.get(i).destroy();
}
for (int i = 0, n = this.subRules.size(); i < n; ++i) {
this.subRules.get(i).onDestroy();
}
}
void scaleStrokeWidth(float scaleFactor) {
for (int i = 0, n = this.renderInstructions.size(); i < n; ++i) {
this.renderInstructions.get(i).scaleStrokeWidth(scaleFactor);
}
for (int i = 0, n = this.subRules.size(); i < n; ++i) {
this.subRules.get(i).scaleStrokeWidth(scaleFactor);
}
}
void scaleTextSize(float scaleFactor) {
for (int i = 0, n = this.renderInstructions.size(); i < n; ++i) {
this.renderInstructions.get(i).scaleTextSize(scaleFactor);
}
for (int i = 0, n = this.subRules.size(); i < n; ++i) {
this.subRules.get(i).scaleTextSize(scaleFactor);
}
}
}