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);
},
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 + '' + rulename + '>\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;
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 @@
+
+
+