diff --git a/src/libkomwm.py b/src/libkomwm.py index 3b93298..53eafe8 100644 --- a/src/libkomwm.py +++ b/src/libkomwm.py @@ -72,18 +72,31 @@ def query_style(args): results = [] for zoom in range(minzoom, maxzoom + 1): - runtime_conditions_arr = [] - + all_runtime_conditions_arr = [] # Get runtime conditions which are used for class 'cl' on zoom 'zoom' if "area" not in cltags: - runtime_conditions_arr.extend(style.get_runtime_rules(clname, "line", cltags, zoom)) - runtime_conditions_arr.extend(style.get_runtime_rules(clname, "area", cltags, zoom)) + all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "line", cltags, zoom)) + all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "area", cltags, zoom)) if "area" not in cltags: - runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom)) + all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom)) - # If there is no any runtime conditions, do not filter style by runtime conditions - if len(runtime_conditions_arr) == 0: + runtime_conditions_arr = [] + if len(all_runtime_conditions_arr) == 0: + # If there is no runtime conditions, do not filter style by runtime conditions runtime_conditions_arr.append(None) + elif len(all_runtime_conditions_arr) == 1: + runtime_conditions_arr = all_runtime_conditions_arr + else: + # Keep unique conditions only + runtime_conditions_arr.append(all_runtime_conditions_arr.pop(0)) + for new_rt_conditions in all_runtime_conditions_arr: + conditions_unique = True + for rt_conditions in runtime_conditions_arr: + if new_rt_conditions == rt_conditions: + conditions_unique = False + break + if conditions_unique: + runtime_conditions_arr.append(new_rt_conditions) for runtime_conditions in runtime_conditions_arr: has_icons_for_areas = False @@ -224,21 +237,24 @@ def komap_mapswithme(options): # Parse style mapcss global style - style = MapCSS(options.minzoom, options.maxzoom + 1) + style = MapCSS(options.minzoom, options.maxzoom) style.parse(filename=options.filename, static_tags=mapcss_static_tags, dynamic_tags=mapcss_dynamic_tags) - # Build optimization tree - class/type -> StyleChoosers + # Build optimization tree - class/zoom/type -> StyleChoosers + clname_cltag_unique = set() for cl in class_order: clname = cl if cl.find('-') == -1 else cl[:cl.find('-')] # Get first tag of the class/type. cltag = next(iter(classificator[cl].keys())) - style.build_choosers_tree(clname, "line", cltag) - style.build_choosers_tree(clname, "area", cltag) - style.build_choosers_tree(clname, "node", cltag) - style.restore_choosers_order("line") - style.restore_choosers_order("area") - style.restore_choosers_order("node") + clname_cltag = clname + '$' + cltag + if clname_cltag not in clname_cltag_unique: + clname_cltag_unique.add(clname_cltag) + style.build_choosers_tree(clname, "line", cltag) + style.build_choosers_tree(clname, "area", cltag) + style.build_choosers_tree(clname, "node", cltag) + + style.finalize_choosers_tree() # Get colors section from style style_colors = {} diff --git a/src/mapcss/Condition.py b/src/mapcss/Condition.py index 9bed7d4..a3b84cb 100644 --- a/src/mapcss/Condition.py +++ b/src/mapcss/Condition.py @@ -33,42 +33,44 @@ class Condition: def test(self, tags): """ - Test a hash against this condition + Test tags against this condition """ t = self.type params = self.params - if t == 'eq': # don't compare tags against sublayers + + if t == 'eq': + # Don't compare tags against sublayers if params[0][:2] == "::": return params[1] - try: - if t == 'eq': - return tags[params[0]] == params[1] - if t == 'ne': - return tags.get(params[0], "") != params[1] - if t == 'regex': - return bool(self.regex.match(tags[params[0]])) - if t == 'true': - return tags.get(params[0]) == 'yes' - if t == 'untrue': - return tags.get(params[0]) == 'no' - if t == 'set': - if params[0] in tags: - return tags[params[0]] != '' - return False - if t == 'unset': - if params[0] in tags: - return tags[params[0]] == '' - return True - if t == '<': - return (Number(tags[params[0]]) < Number(params[1])) - if t == '<=': - return (Number(tags[params[0]]) <= Number(params[1])) - if t == '>': - return (Number(tags[params[0]]) > Number(params[1])) - if t == '>=': - return (Number(tags[params[0]]) >= Number(params[1])) - except KeyError: - pass + return (params[0] in tags and tags[params[0]] == params[1]) + if t == 'ne': + return (params[0] not in tags or tags[params[0]] != params[1]) + if t == 'true': + return tags.get(params[0]) == 'yes' + if t == 'untrue': + return tags.get(params[0]) == 'no' + if t == 'set': + if params[0] in tags: + return tags[params[0]] != '' + return False + if t == 'unset': + if params[0] in tags: + return tags[params[0]] == '' + return True + + if params[0] not in tags: + return False + if t == 'regex': + return bool(self.regex.match(tags[params[0]])) + if t == '<': + return (Number(tags[params[0]]) < Number(params[1])) + if t == '<=': + return (Number(tags[params[0]]) <= Number(params[1])) + if t == '>': + return (Number(tags[params[0]]) > Number(params[1])) + if t == '>=': + return (Number(tags[params[0]]) >= Number(params[1])) + return False def __repr__(self): diff --git a/src/mapcss/Rule.py b/src/mapcss/Rule.py index ede2543..db516c1 100644 --- a/src/mapcss/Rule.py +++ b/src/mapcss/Rule.py @@ -25,7 +25,7 @@ type_matches = { class Rule(): def __init__(self, s=''): - self.runtime_conditions = [] + self.runtime_conditions = None self.conditions = [] # self.isAnd = True self.minZoom = 0 @@ -33,17 +33,12 @@ class Rule(): if s == "*": s = "" self.subject = s # "", "way", "node" or "relation" + self.type_matches = type_matches[s] if s in type_matches else set() def __repr__(self): return "%s|z%s-%s %s %s" % (self.subject, self.minZoom, self.maxZoom, self.conditions, self.runtime_conditions) - def test(self, obj, tags, zoom): - if (zoom < self.minZoom) or (zoom > self.maxZoom): - return False - - if (self.subject != '') and not _test_feature_compatibility(obj, self.subject, tags): - return False - + def test(self, tags): subpart = "::default" for condition in self.conditions: res = condition.test(tags) @@ -66,28 +61,3 @@ class Rule(): return set(["*"]) return a - - -def _test_feature_compatibility(f1, f2, tags={}): - """ - Checks if feature of type f1 is compatible with f2. - """ - if f2 == f1: - return True - if f2 not in ("way", "area", "line"): - return False - elif f2 == "way" and f1 == "line": - return True - elif f2 == "way" and f1 == "area": - return True - elif f2 == "area" and f1 in ("way", "area"): -# if ":area" in tags: - return True -# else: -# return False - elif f2 == "line" and f1 in ("way", "line", "area"): - return True - else: - return False - # print f1, f2, True - return True diff --git a/src/mapcss/StyleChooser.py b/src/mapcss/StyleChooser.py index 2444152..a386599 100644 --- a/src/mapcss/StyleChooser.py +++ b/src/mapcss/StyleChooser.py @@ -87,6 +87,7 @@ class StyleChooser: self.selzooms = None self.compatible_types = set() self.has_evals = False + self.has_runtime_conditions = False self.cached_tags = None def extract_tags(self): @@ -96,62 +97,34 @@ class StyleChooser: for r in self.ruleChains: a.update(r.extract_tags()) if "*" in a: - a.clear() - a.add("*") + a = set('*') break if self.has_evals and "*" not in a: for s in self.styles: for v in list(s.values()): if type(v) == self.eval_type: a.update(v.extract_tags()) - if "*" in a or len(a) == 0: - a.clear() - a.add("*") + if len(a) == 0: + a = set('*') self.cached_tags = a return a - def get_runtime_conditions(self, ftype, tags, zoom): - if self.selzooms: - if zoom < self.selzooms[0] or zoom > self.selzooms[1]: - return None + def get_runtime_conditions(self, tags): + if not self.has_runtime_conditions: + return None - rule_and_object_id = self.testChain(self.ruleChains, ftype, tags, zoom) + rule_and_object_id = self.testChains(tags) if not rule_and_object_id: return None rule = rule_and_object_id[0] - if (len(rule.runtime_conditions) == 0): - return None - return rule.runtime_conditions - def isCorrespondingRule(self, filter_by_runtime_conditions, rule): - # If rule can be applied according to runtime conditions, then - # function return true, else it returns false - if len(rule.runtime_conditions) == 0: - return True - if filter_by_runtime_conditions is None: - return True - if filter_by_runtime_conditions == rule.runtime_conditions: - return True - # Actually we should check rule.runtime_conditions is a subset of filter_by_runtime_conditions - for r in rule.runtime_conditions: - if r not in filter_by_runtime_conditions: - return False - return True - - def updateStyles(self, sl, ftype, tags, zoom, xscale, zscale, filter_by_runtime_conditions): + def updateStyles(self, sl, tags, xscale, zscale, filter_by_runtime_conditions): # Are any of the ruleChains fulfilled? - if self.selzooms: - if zoom < self.selzooms[0] or zoom > self.selzooms[1]: - return sl - - #if ftype not in self.compatible_types: - #return sl - - rule_and_object_id = self.testChain(self.ruleChains, ftype, tags, zoom) + rule_and_object_id = self.testChains(tags) if not rule_and_object_id: return sl @@ -159,7 +132,9 @@ class StyleChooser: rule = rule_and_object_id[0] object_id = rule_and_object_id[1] - if not self.isCorrespondingRule(filter_by_runtime_conditions, rule): + if (filter_by_runtime_conditions is not None + and rule.runtime_conditions is not None + and filter_by_runtime_conditions != rule.runtime_conditions): return sl for r in self.styles: @@ -203,12 +178,12 @@ class StyleChooser: return sl - def testChain(self, chain, obj, tags, zoom): + def testChains(self, tags): """ Tests an object against a chain """ - for r in chain: - tt = r.test(obj, tags, zoom) + for r in self.ruleChains: + tt = r.test(tags) if tt: return r, tt return False @@ -249,8 +224,11 @@ class StyleChooser: """ adds into the current ruleChain (existing Rule) """ - self.ruleChains[-1].runtime_conditions.append(c) - self.ruleChains[-1].runtime_conditions.sort() + if self.ruleChains[-1].runtime_conditions is None: + self.ruleChains[-1].runtime_conditions = [c] + self.has_runtime_conditions = True + else: + self.ruleChains[-1].runtime_conditions.append(c) def addStyles(self, a): # print "addStyle ", a diff --git a/src/mapcss/__init__.py b/src/mapcss/__init__.py index 13bfd13..d52e8a5 100644 --- a/src/mapcss/__init__.py +++ b/src/mapcss/__init__.py @@ -96,7 +96,7 @@ class MapCSS(): self.scalepair = (minscale, maxscale) self.choosers = [] self.choosers_by_type = {} - self.choosers_by_type_and_tag = {} + self.choosers_by_type_zoom_tag = {} self.variables = {} self.style_loaded = False @@ -113,44 +113,61 @@ class MapCSS(): logging.error("unparsed zoom: %s" % s) def build_choosers_tree(self, clname, type, cltag): - if type not in self.choosers_by_type_and_tag: - self.choosers_by_type_and_tag[type] = {} - if clname not in self.choosers_by_type_and_tag[type]: - self.choosers_by_type_and_tag[type][clname] = set() + if type not in self.choosers_by_type_zoom_tag: + self.choosers_by_type_zoom_tag[type] = {} + for zoom in range(self.minscale, self.maxscale + 1): + if zoom not in self.choosers_by_type_zoom_tag[type]: + self.choosers_by_type_zoom_tag[type][zoom] = {} + if clname not in self.choosers_by_type_zoom_tag[type][zoom]: + self.choosers_by_type_zoom_tag[type][zoom][clname] = {'arr': [], 'set': set()} if type in self.choosers_by_type: for chooser in self.choosers_by_type[type]: - for tag in chooser.extract_tags(): - if tag == "*" or tag == cltag: - if chooser not in self.choosers_by_type_and_tag[type][clname]: - self.choosers_by_type_and_tag[type][clname].add(chooser) - break + chooser_tags = chooser.extract_tags() + if '*' in chooser_tags or cltag in chooser_tags: + for zoom in range(int(chooser.selzooms[0]), int(chooser.selzooms[1]) + 1): + if chooser not in self.choosers_by_type_zoom_tag[type][zoom][clname]['set']: + self.choosers_by_type_zoom_tag[type][zoom][clname]['arr'].append(chooser) + self.choosers_by_type_zoom_tag[type][zoom][clname]['set'].add(chooser) + + def finalize_choosers_tree(self): + for ftype in self.choosers_by_type_zoom_tag.keys(): + for zoom in self.choosers_by_type_zoom_tag[ftype].keys(): + for clname in self.choosers_by_type_zoom_tag[ftype][zoom].keys(): + # Discard unneeded unique set of choosers. + self.choosers_by_type_zoom_tag[ftype][zoom][clname] = self.choosers_by_type_zoom_tag[ftype][zoom][clname]['arr'] + for i in range(0, len(self.choosers_by_type_zoom_tag[ftype][zoom][clname])): + chooser = self.choosers_by_type_zoom_tag[ftype][zoom][clname][i] + optimized = StyleChooser(chooser.scalepair) + optimized.styles = chooser.styles + optimized.eval_type = chooser.eval_type + optimized.has_evals = chooser.has_evals + optimized.has_runtime_conditions = chooser.has_runtime_conditions + optimized.selzooms = [zoom, zoom] + optimized.ruleChains = [] + for rule in chooser.ruleChains: + # Discard chooser's rules that don't match type or zoom. + if ftype in rule.type_matches and zoom >= rule.minZoom and zoom <= rule.maxZoom: + optimized.ruleChains.append(rule) + self.choosers_by_type_zoom_tag[ftype][zoom][clname][i] = optimized - def restore_choosers_order(self, type): - ethalon_choosers = self.choosers_by_type[type] - for tag, choosers_for_tag in list(self.choosers_by_type_and_tag[type].items()): - tmp = [] - for ec in ethalon_choosers: - if ec in choosers_for_tag: - tmp.append(ec) - self.choosers_by_type_and_tag[type][tag] = tmp def get_runtime_rules(self, clname, type, tags, zoom): """ Returns array of runtime_conditions which are used for clname/type/tags/zoom """ runtime_rules = [] - if type in self.choosers_by_type_and_tag: - for chooser in self.choosers_by_type_and_tag[type][clname]: - runtime_conditions = chooser.get_runtime_conditions(type, tags, zoom) + if type in self.choosers_by_type_zoom_tag: + for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]: + runtime_conditions = chooser.get_runtime_conditions(tags) if runtime_conditions: runtime_rules.append(runtime_conditions) return runtime_rules def get_style(self, clname, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions): style = [] - if type in self.choosers_by_type_and_tag: - for chooser in self.choosers_by_type_and_tag[type][clname]: - style = chooser.updateStyles(style, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions) + if type in self.choosers_by_type_zoom_tag: + for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]: + style = chooser.updateStyles(style, tags, xscale, zscale, filter_by_runtime_conditions) style = [x for x in style if x["object-id"] != "::*"] for x in style: for k, v in [('width', 0), ('casing-width', 0)]: