From 25ee4301c00dd23ef9e6ca3314c6e27e183a46b2 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Fri, 16 May 2014 17:51:08 -0400 Subject: [PATCH 1/3] Parse group symbolizer, matching internal group rules by name and class. --- lib/carto/renderer.js | 68 ++++++++++++++++++++++----- lib/carto/tree/definition.js | 91 ++++++++++++++++++++++++++++++++---- lib/carto/tree/filterset.js | 5 +- lib/carto/tree/zoom.js | 7 +-- 4 files changed, 146 insertions(+), 25 deletions(-) diff --git a/lib/carto/renderer.js b/lib/carto/renderer.js index dc5df7d2e..44851dffe 100644 --- a/lib/carto/renderer.js +++ b/lib/carto/renderer.js @@ -104,32 +104,78 @@ carto.Renderer.prototype.render = function render(m) { function appliesTo(name, classIndex) { return function(definition) { - return definition.appliesTo(l.name, classIndex); + return definition.appliesTo(name, classIndex); }; } + /** + * Collect all applicable rules from mss files for given style name and classes. + * @param {Array} definitions Definition objects from mss files + * @param {String} name style name to select by + * @param {String} classList space separated list of style classes to select by + */ + function collectRules(definitions, name, classList) { + var classIndex = {}, rules, matching; + + // Classes are given as space-separated alphanumeric strings. + var classes = (classList || '').split(/\s+/g); + for (var j = 0; j < classes.length; j++) { + classIndex[classes[j]] = true; + } + matching = definitions.filter(appliesTo(name, classIndex)); + rules = inheritDefinitions(matching, env); + + return sortStyles(rules, env); + } + + /** + * Find any symbolizers in given definition that contain child rules. + * Collect applicable child rules from mss files and add them to the definition. + * @param {Object} definition Definition object to populate with child rules + * @param {Object} childRuleCache keeps track of rule sets that were already collected + */ + function collectChildRules(def, childRuleCache) { + var existing = {}, childrules = def.collectChildRuleIdentifiers(env, existing); + var nameKey, classKey; + for (var zoom in childrules) { + perzoom = childrules[zoom]; + for (var key in perzoom) { + symbolizer = perzoom[key]; + if (Object.keys(symbolizer).length >= 0) { + nameKey = symbolizer['style-name'] || '__none__'; + classKey = symbolizer['style-class'] || '__none__'; + if (!childRuleCache[nameKey]) { + childRuleCache[nameKey] = {}; + } + if (!childRuleCache[nameKey][classKey]) { + childRuleCache[nameKey][classKey] = collectRules(definitions, symbolizer['style-name'], symbolizer['style-class']); + } + symbolizer.rules = childRuleCache[nameKey][classKey]; + } + } + } + def.childrules = childrules; + } + // Iterate through layers and create styles custom-built // for each of them, and apply those styles to the layers. - var styles, l, classIndex, rules, sorted, matching; + var styles, l, sorted, childRuleCache = {}; for (var i = 0; i < m.Layer.length; i++) { l = m.Layer[i]; styles = []; - classIndex = {}; if (env.benchmark) console.warn('processing layer: ' + l.id); - // Classes are given as space-separated alphanumeric strings. - var classes = (l['class'] || '').split(/\s+/g); - for (var j = 0; j < classes.length; j++) { - classIndex[classes[j]] = true; - } - matching = definitions.filter(appliesTo(l.name, classIndex)); - rules = inheritDefinitions(matching, env); - sorted = sortStyles(rules, env); + sorted = collectRules(definitions, l.name, l['class']); for (var k = 0, rule, style_name; k < sorted.length; k++) { rule = sorted[k]; style_name = l.name + (rule.attachment !== '__default__' ? '-' + rule.attachment : ''); + // Iterate through definitions and collect child rules + for (var p = 0; p < rule.length; p++) { + collectChildRules(rule[p], childRuleCache); + } + // env.effects can be modified by this call var styleXML = carto.tree.StyleXML(style_name, rule.attachment, rule, env); diff --git a/lib/carto/tree/definition.js b/lib/carto/tree/definition.js index 140888779..7b1cd8f03 100644 --- a/lib/carto/tree/definition.js +++ b/lib/carto/tree/definition.js @@ -39,6 +39,7 @@ tree.Definition.prototype.clone = function(filters) { clone.ruleIndex = _.clone(this.ruleIndex); clone.filters = filters ? filters : this.filters.clone(); clone.attachment = this.attachment; + clone.childrules = this.childrules; return clone; }; @@ -82,8 +83,9 @@ function symbolizerList(sym_order) { .map(function(v) { return v[0]; }); } -tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) { - var xml = zoom.toXML(env).join('') + this.filters.toXML(env); +tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom, childrules, indentation, rulename) { + var xml = zoom.toXML(env, indentation + 1).join('') + this.filters.toXML(env, indentation + 1); + var indent = new Array(indentation + 1).join(' '); // Sort symbolizers by the index of their first property definition var sym_order = [], indexes = []; @@ -120,8 +122,8 @@ tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) { var name = symbolizerName(symbolizer); - var selfclosing = true, tagcontent; - xml += ' <' + name + ' '; + var selfclosing = true, tagcontent = '', hasrules = false, childname; + xml += indent + ' <' + name + ' '; for (var j in attributes) { if (symbolizer === 'map') env.error({ message: 'Map properties are not permitted in other rules', @@ -129,16 +131,30 @@ tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) { filename: attributes[j].filename }); var x = tree.Reference.selector(attributes[j].name); - if (x && x.serialization && x.serialization === 'content') { + if (x && x.serialization && x.serialization === 'rules') { + hasrules = true; + childname = x['tag-name']; + } else if (x && x.serialization && x.serialization === 'content') { selfclosing = false; tagcontent = attributes[j].ev(env).toXML(env, true); } else if (x && x.serialization && x.serialization === 'tag') { selfclosing = false; - tagcontent = attributes[j].ev(env).toXML(env, true); + tagcontent += attributes[j].ev(env).toXML(env, true); } else { xml += attributes[j].ev(env).toXML(env) + ' '; } } + // Insert xml for child rules applicable to this symbolizer + if (hasrules && childrules[sym_order[i]] + && childrules[sym_order[i]].rules + && childrules[sym_order[i]].rules[0]) { + selfclosing = false; + var existing = {}; + var ruletags = childrules[sym_order[i]].rules[0].map(function(def) { + return def.toXML(env, existing, indentation + 2, childname); + }); + tagcontent += '\n' + ruletags.join('') + indent + ' '; + } if (selfclosing) { xml += '/>\n'; } else if (typeof tagcontent !== "undefined") { @@ -150,7 +166,7 @@ tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) { } } if (!sym_count || !xml) return ''; - return ' \n' + xml + ' \n'; + return indent + '<' + rulename + '>\n' + xml + indent + '\n'; }; // Take a zoom range of zooms and 'i', the index of a rule in this.rules, @@ -184,7 +200,9 @@ tree.Definition.prototype.collectSymbolizers = function(zooms, i) { // when using the filter-mode="first", more specific zoom filters will always // end up before broader ranges. The filter-mode will pick those first before // resorting to the zoom range with the hole and stop processing further rules. -tree.Definition.prototype.toXML = function(env, existing) { +tree.Definition.prototype.toXML = function(env, existing, indentation, rulename) { + indentation = indentation || 1; + rulename = rulename || 'Rule'; var filter = this.filters.toString(); if (!(filter in existing)) existing[filter] = tree.Zoom.all; @@ -198,7 +216,9 @@ tree.Definition.prototype.toXML = function(env, existing) { if (symbolizers = this.collectSymbolizers(zooms, i)) { if (!(existing[filter] & zooms.current)) continue; xml += this.symbolizersToXML(env, symbolizers, - (new tree.Zoom()).setZoom(existing[filter] & zooms.current)); + (new tree.Zoom()).setZoom(existing[filter] & zooms.current), + this.childrules ? this.childrules[zooms.current] : {}, + indentation, rulename); existing[filter] &= ~zooms.current; } } @@ -207,4 +227,57 @@ tree.Definition.prototype.toXML = function(env, existing) { return xml; }; +// Take a zoom range of zooms and 'i', the index of a rule in this.rules, +// and finds any name and class identifiers for child rules. +tree.Definition.prototype.collectChildRuleIdentifiersAtZoom = function(zooms, i) { + var identifiers = {}, child; + + for (var j = i; j < this.rules.length; j++) { + child = this.rules[j]; + var key = child.instance + '/' + child.symbolizer; + var name = tree.Reference.selectorName(child.name); + if (zooms.current & child.zoom && + (!(key in identifiers) || + (!(name in identifiers[key])))) { + zooms.current &= child.zoom; + if ((name === "style-name" || name === "style-class")) { + if (!(key in identifiers)) { + identifiers[key] = {}; + } + identifiers[key][name] = child.value.toString(); + } + } + } + + zooms.rule &= (zooms.available &= ~zooms.current); + return identifiers; +}; + +/** + * Collect name and class identifiers for child rules in any symbolizer across all zoom levels. + * @param {Object} env the current environment + * @param {Object} existing existing rules +*/ +tree.Definition.prototype.collectChildRuleIdentifiers = function(env, existing) { + var filter = this.filters.toString(); + if (!(filter in existing)) existing[filter] = tree.Zoom.all; + + var available = tree.Zoom.all, xml = '', zoom, symbolizers, identifiers = {}, + zooms = { available: tree.Zoom.all }; + for (var i = 0; i < this.rules.length && available; i++) { + zooms.rule = this.rules[i].zoom; + if (!(existing[filter] & zooms.rule)) continue; + + while (zooms.current = zooms.rule & available) { + if (symbolizers = this.collectChildRuleIdentifiersAtZoom(zooms, i)) { + if (!(existing[filter] & zooms.current)) continue; + identifiers[zooms.current] = symbolizers; + existing[filter] &= ~zooms.current; + } + } + } + + return identifiers; +}; + })(require('../tree')); diff --git a/lib/carto/tree/filterset.js b/lib/carto/tree/filterset.js index 4e3642b32..4c530ba15 100644 --- a/lib/carto/tree/filterset.js +++ b/lib/carto/tree/filterset.js @@ -4,13 +4,14 @@ tree.Filterset = function Filterset() { this.filters = {}; }; -tree.Filterset.prototype.toXML = function(env) { +tree.Filterset.prototype.toXML = function(env, indentation) { + var indent = new Array((indentation || 2) + 1).join(' '); var filters = []; for (var id in this.filters) { filters.push('(' + this.filters[id].toXML(env).trim() + ')'); } if (filters.length) { - return ' ' + filters.join(' and ') + '\n'; + return indent + '' + filters.join(' and ') + '\n'; } else { return ''; } diff --git a/lib/carto/tree/zoom.js b/lib/carto/tree/zoom.js index 3dd092159..656e3df1e 100644 --- a/lib/carto/tree/zoom.js +++ b/lib/carto/tree/zoom.js @@ -91,7 +91,8 @@ tree.Zoom.ranges = { }; // Only works for single range zooms. `[XXX....XXXXX.........]` is invalid. -tree.Zoom.prototype.toXML = function() { +tree.Zoom.prototype.toXML = function(env, indentation) { + var indent = new Array((indentation || 2) + 1).join(' '); var conditions = []; if (this.zoom != tree.Zoom.all) { var start = null, end = null; @@ -101,9 +102,9 @@ tree.Zoom.prototype.toXML = function() { end = i; } } - if (start > 0) conditions.push(' ' + + if (start > 0) conditions.push(indent + '' + tree.Zoom.ranges[start] + '\n'); - if (end < 22) conditions.push(' ' + + if (end < 22) conditions.push(indent + '' + tree.Zoom.ranges[end + 1] + '\n'); } return conditions; From e95ae2f8efdade84a67b96fa0c44e16a237abd02 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Wed, 28 May 2014 08:23:30 -0400 Subject: [PATCH 2/3] Add functions to generate group layout xml. --- lib/carto/functions.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/carto/functions.js b/lib/carto/functions.js index f5c3b7f99..2fcc6dd0b 100644 --- a/lib/carto/functions.js +++ b/lib/carto/functions.js @@ -29,6 +29,37 @@ tree.functions = { } }; }, + simplelayout: function () { + var margin; + if (arguments.length > 0) margin = arguments[0]; + + return { + is: 'tag', + margin: margin, + toString: function(env) { + return ''; + } + }; + }, + pairlayout: function () { + var margin, maxdiff; + if (arguments.length > 0) margin = arguments[0]; + if (arguments.length > 1) maxdiff = arguments[1]; + + return { + is: 'tag', + margin: margin, + maxdiff: maxdiff, + toString: function(env) { + return ''; + } + }; + }, hsl: function (h, s, l) { return this.hsla(h, s, l, 1.0); }, From ad08a37eef0f27c268a89425e028ae9b956a1329 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Thu, 29 May 2014 09:29:13 -0400 Subject: [PATCH 3/3] Add tests for group symbolizer rendering. --- test/rendering-mss/group_layout.mss | 15 ++ test/rendering-mss/group_layout.xml | 25 ++++ test/rendering/group_symbolizer.mml | 14 ++ test/rendering/group_symbolizer.mss | 62 +++++++++ test/rendering/group_symbolizer.result | 182 +++++++++++++++++++++++++ 5 files changed, 298 insertions(+) create mode 100644 test/rendering-mss/group_layout.mss create mode 100644 test/rendering-mss/group_layout.xml create mode 100644 test/rendering/group_symbolizer.mml create mode 100644 test/rendering/group_symbolizer.mss create mode 100644 test/rendering/group_symbolizer.result diff --git a/test/rendering-mss/group_layout.mss b/test/rendering-mss/group_layout.mss new file mode 100644 index 000000000..9eb2b6bd6 --- /dev/null +++ b/test/rendering-mss/group_layout.mss @@ -0,0 +1,15 @@ +#layer { + group-layout: simplelayout(); +} +#layer[zoom>2] { + group-layout: pairlayout(); +} +#layer[zoom>4] { + group-layout: simplelayout(1); +} +#layer[zoom>6] { + group-layout: pairlayout(2); +} +#layer[zoom>8] { + group-layout: pairlayout(2, 14); +} \ No newline at end of file diff --git a/test/rendering-mss/group_layout.xml b/test/rendering-mss/group_layout.xml new file mode 100644 index 000000000..19eca63d4 --- /dev/null +++ b/test/rendering-mss/group_layout.xml @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/test/rendering/group_symbolizer.mml b/test/rendering/group_symbolizer.mml new file mode 100644 index 000000000..cea5f6b00 --- /dev/null +++ b/test/rendering/group_symbolizer.mml @@ -0,0 +1,14 @@ +{ + "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over", + "Stylesheet": [ + "group_symbolizer.mss" + ], + "Layer": [{ + "name": "world", + "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over", + "Datasource": { + "file": "http://tilemill-data.s3.amazonaws.com/test_data/shape_demo.zip", + "type": "shape" + } + }] +} diff --git a/test/rendering/group_symbolizer.mss b/test/rendering/group_symbolizer.mss new file mode 100644 index 000000000..498aae64c --- /dev/null +++ b/test/rendering/group_symbolizer.mss @@ -0,0 +1,62 @@ +.shield { + shield-file: url('generic_shield.svg'); + shield-name: [shield_num%]; + shield-face-name: "DejaVu Sans Bold"; + text-name: [above_text%]; + text-face-name: "DejaVu Sans Book"; + text-dy: -15; +} + +#shield-us { + shield-file: url('state_highway.svg'); + [[type%]="I"] { + shield-file: url('interstate.svg'); + shield-fill: white; + } + [[type%]="US"] { + shield-file: url('us_highway.svg'); + } +} + +#shield-canada[[type%]="TCH"] { + shield-file: url('trans_canada_highway_small.svg'); + shield-fill: green; + [zoom>15] { + shield-file: url('trans_canada_highway_large.svg'); + } +} + +#shield-canada[[type%]="QC"] { + shield-file: url('quebec_highway.svg'); +} + +.shield-minor[[type%]="CR"] { + shield-face-name: "DejaVu Sans Bold"; + shield-file: url('images/county_route.svg'); +} + +#secondary { + text-face-name: "DejaVu Sans Book"; + text-name: [name]; +} + +#world { + group-num-columns: 2; + group-class: "shield"; + group-layout: simplelayout(); + [zoom>12] { + group-class: "shield shield-minor"; + } + [country="USA"] { + group-layout: pairlayout(1); + group-name: "shield-us"; + } + [country="CAN"] { + group-name: "shield-canada"; + [zoom>8] { + secondary/group-name: "secondary"; + secondary/group-num-columns: 0; + secondary/group-layout: simplelayout(); + } + } +} \ No newline at end of file diff --git a/test/rendering/group_symbolizer.result b/test/rendering/group_symbolizer.result new file mode 100644 index 000000000..d781964c0 --- /dev/null +++ b/test/rendering/group_symbolizer.result @@ -0,0 +1,182 @@ + + + + + + + + world + + + + + + +