Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Group symbolizer parse and render #349

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions lib/carto/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<SimpleLayout ' +
(margin ? 'item-margin="' + margin.ev(env) + '" ' : '') +
'/>';
}
};
},
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 '<PairLayout ' +
(margin ? 'item-margin="' + margin.ev(env) + '" ' : '') +
(maxdiff ? 'max-difference="' + maxdiff.ev(env) + '" ' : '') +
'/>';
}
};
},
hsl: function (h, s, l) {
return this.hsla(h, s, l, 1.0);
},
Expand Down
68 changes: 57 additions & 11 deletions lib/carto/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
91 changes: 82 additions & 9 deletions lib/carto/tree/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -120,25 +122,39 @@ 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',
index: attributes[j].index,
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") {
Expand All @@ -150,7 +166,7 @@ tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) {
}
}
if (!sym_count || !xml) return '';
return ' <Rule>\n' + xml + ' </Rule>\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,
Expand Down Expand Up @@ -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;

Expand 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;
}
}
Expand All @@ -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'));
5 changes: 3 additions & 2 deletions lib/carto/tree/filterset.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' <Filter>' + filters.join(' and ') + '</Filter>\n';
return indent + '<Filter>' + filters.join(' and ') + '</Filter>\n';
} else {
return '';
}
Expand Down
7 changes: 4 additions & 3 deletions lib/carto/tree/zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -101,9 +102,9 @@ tree.Zoom.prototype.toXML = function() {
end = i;
}
}
if (start > 0) conditions.push(' <MaxScaleDenominator>' +
if (start > 0) conditions.push(indent + '<MaxScaleDenominator>' +
tree.Zoom.ranges[start] + '</MaxScaleDenominator>\n');
if (end < 22) conditions.push(' <MinScaleDenominator>' +
if (end < 22) conditions.push(indent + '<MinScaleDenominator>' +
tree.Zoom.ranges[end + 1] + '</MinScaleDenominator>\n');
}
return conditions;
Expand Down
15 changes: 15 additions & 0 deletions test/rendering-mss/group_layout.mss
Original file line number Diff line number Diff line change
@@ -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);
}
25 changes: 25 additions & 0 deletions test/rendering-mss/group_layout.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Style name="style" filter-mode="first">
<Rule>
<MaxScaleDenominator>1500000</MaxScaleDenominator>
<GroupSymbolizer ><PairLayout item-margin="2" max-difference="14" /></GroupSymbolizer>
</Rule>
<Rule>
<MaxScaleDenominator>6500000</MaxScaleDenominator>
<MinScaleDenominator>1500000</MinScaleDenominator>
<GroupSymbolizer ><PairLayout item-margin="2" /></GroupSymbolizer>
</Rule>
<Rule>
<MaxScaleDenominator>25000000</MaxScaleDenominator>
<MinScaleDenominator>6500000</MinScaleDenominator>
<GroupSymbolizer ><SimpleLayout item-margin="1" /></GroupSymbolizer>
</Rule>
<Rule>
<MaxScaleDenominator>100000000</MaxScaleDenominator>
<MinScaleDenominator>25000000</MinScaleDenominator>
<GroupSymbolizer ><PairLayout /></GroupSymbolizer>
</Rule>
<Rule>
<MinScaleDenominator>100000000</MinScaleDenominator>
<GroupSymbolizer ><SimpleLayout /></GroupSymbolizer>
</Rule>
</Style>
14 changes: 14 additions & 0 deletions test/rendering/group_symbolizer.mml
Original file line number Diff line number Diff line change
@@ -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"
}
}]
}
Loading