You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
403 lines
14 KiB
403 lines
14 KiB
var _ = require('underscore');
|
|
var carto = require('./index');
|
|
|
|
carto.Renderer = function Renderer(env, options) {
|
|
this.env = env || {};
|
|
this.options = options || {};
|
|
this.options.mapnik_version = this.options.mapnik_version || 'latest';
|
|
};
|
|
|
|
/**
|
|
* Prepare a MSS document (given as an string) into a
|
|
* XML Style fragment (mostly useful for debugging)
|
|
*
|
|
* @param {String} data the mss contents as a string.
|
|
*/
|
|
carto.Renderer.prototype.renderMSS = function render(data) {
|
|
// effects is a container for side-effects, which currently
|
|
// are limited to FontSets.
|
|
var env = _(this.env).defaults({
|
|
benchmark: false,
|
|
validation_data: false,
|
|
effects: []
|
|
});
|
|
|
|
if (!carto.tree.Reference.setVersion(this.options.mapnik_version)) {
|
|
throw new Error("Could not set mapnik version to " + this.options.mapnik_version);
|
|
}
|
|
|
|
var output = [];
|
|
var styles = [];
|
|
|
|
if (env.benchmark) console.time('Parsing MSS');
|
|
var parser = (carto.Parser(env)).parse(data);
|
|
if (env.benchmark) console.timeEnd('Parsing MSS');
|
|
|
|
if (env.benchmark) console.time('Rule generation');
|
|
var rule_list = parser.toList(env);
|
|
if (env.benchmark) console.timeEnd('Rule generation');
|
|
|
|
if (env.benchmark) console.time('Rule inheritance');
|
|
var rules = inheritDefinitions(rule_list, env);
|
|
if (env.benchmark) console.timeEnd('Rule inheritance');
|
|
|
|
if (env.benchmark) console.time('Style sort');
|
|
var sorted = sortStyles(rules,env);
|
|
if (env.benchmark) console.timeEnd('Style sort');
|
|
|
|
if (env.benchmark) console.time('Total Style generation');
|
|
for (var k = 0, rule, style_name; k < sorted.length; k++) {
|
|
rule = sorted[k];
|
|
style_name = 'style' + (rule.attachment !== '__default__' ? '-' + rule.attachment : '');
|
|
styles.push(style_name);
|
|
var bench_name = '\tStyle "'+style_name+'" (#'+k+') toXML';
|
|
if (env.benchmark) console.time(bench_name);
|
|
// env.effects can be modified by this call
|
|
output.push(carto.tree.StyleXML(style_name, rule.attachment, rule, env));
|
|
if (env.benchmark) console.timeEnd(bench_name);
|
|
}
|
|
if (env.benchmark) console.timeEnd('Total Style generation');
|
|
if (env.errors) throw env.errors;
|
|
return output.join('\n');
|
|
};
|
|
|
|
/**
|
|
* Prepare a MML document (given as an object) into a
|
|
* fully-localized XML file ready for Mapnik2 consumption
|
|
*
|
|
* @param {String} m - the JSON file as a string.
|
|
*/
|
|
carto.Renderer.prototype.render = function render(m) {
|
|
// effects is a container for side-effects, which currently
|
|
// are limited to FontSets.
|
|
var env = _(this.env).defaults({
|
|
benchmark: false,
|
|
validation_data: false,
|
|
effects: [],
|
|
ppi: 90.714
|
|
});
|
|
|
|
if (!carto.tree.Reference.setVersion(this.options.mapnik_version)) {
|
|
throw new Error("Could not set mapnik version to " + this.options.mapnik_version);
|
|
}
|
|
|
|
var output = [];
|
|
|
|
// Transform stylesheets into definitions.
|
|
var definitions = _(m.Stylesheet).chain()
|
|
.map(function(s) {
|
|
if (typeof s == 'string') {
|
|
throw new Error("Stylesheet object is expected not a string: '" + s + "'");
|
|
}
|
|
// Passing the environment from stylesheet to stylesheet,
|
|
// allows frames and effects to be maintained.
|
|
env = _(env).extend({filename:s.id});
|
|
|
|
var time = +new Date(),
|
|
root = (carto.Parser(env)).parse(s.data);
|
|
if (env.benchmark)
|
|
console.warn('Parsing time: ' + (new Date() - time) + 'ms');
|
|
return root.toList(env);
|
|
})
|
|
.flatten()
|
|
.value();
|
|
|
|
function appliesTo(name, classIndex) {
|
|
return function(definition) {
|
|
return definition.appliesTo(l.name, classIndex);
|
|
};
|
|
}
|
|
|
|
// 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;
|
|
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);
|
|
|
|
for (var k = 0, rule, style_name; k < sorted.length; k++) {
|
|
rule = sorted[k];
|
|
style_name = l.name + (rule.attachment !== '__default__' ? '-' + rule.attachment : '');
|
|
|
|
// env.effects can be modified by this call
|
|
var styleXML = carto.tree.StyleXML(style_name, rule.attachment, rule, env);
|
|
|
|
if (styleXML) {
|
|
output.push(styleXML);
|
|
styles.push(style_name);
|
|
}
|
|
}
|
|
|
|
output.push(carto.tree.LayerXML(l, styles));
|
|
}
|
|
|
|
output.unshift(env.effects.map(function(e) {
|
|
return e.toXML(env);
|
|
}).join('\n'));
|
|
|
|
var map_properties = getMapProperties(m, definitions, env);
|
|
|
|
// Exit on errors.
|
|
if (env.errors) throw env.errors;
|
|
|
|
// Pass TileJSON and other custom parameters through to Mapnik XML.
|
|
var parameters = _(m).reduce(function(memo, v, k) {
|
|
if (!v && v !== 0) return memo;
|
|
|
|
switch (k) {
|
|
// Known skippable properties.
|
|
case 'srs':
|
|
case 'Layer':
|
|
case 'Stylesheet':
|
|
break;
|
|
// Non URL-bound TileJSON properties.
|
|
case 'bounds':
|
|
case 'center':
|
|
case 'minzoom':
|
|
case 'maxzoom':
|
|
case 'version':
|
|
memo.push(' <Parameter name="' + k + '">' + v + '</Parameter>');
|
|
break;
|
|
// Properties that require CDATA.
|
|
case 'name':
|
|
case 'description':
|
|
case 'legend':
|
|
case 'attribution':
|
|
case 'template':
|
|
memo.push(' <Parameter name="' + k + '"><![CDATA[' + v + ']]></Parameter>');
|
|
break;
|
|
// Mapnik image format.
|
|
case 'format':
|
|
memo.push(' <Parameter name="' + k + '">' + v + '</Parameter>');
|
|
break;
|
|
// Mapnik interactivity settings.
|
|
case 'interactivity':
|
|
memo.push(' <Parameter name="interactivity_layer">' + v.layer + '</Parameter>');
|
|
memo.push(' <Parameter name="interactivity_fields">' + v.fields + '</Parameter>');
|
|
break;
|
|
// Support any additional scalar properties.
|
|
default:
|
|
if ('string' === typeof v) {
|
|
memo.push(' <Parameter name="' + k + '"><![CDATA[' + v + ']]></Parameter>');
|
|
} else if ('number' === typeof v) {
|
|
memo.push(' <Parameter name="' + k + '">' + v + '</Parameter>');
|
|
} else if ('boolean' === typeof v) {
|
|
memo.push(' <Parameter name="' + k + '">' + v + '</Parameter>');
|
|
}
|
|
break;
|
|
}
|
|
return memo;
|
|
}, []);
|
|
if (parameters.length) output.unshift(
|
|
'<Parameters>\n' +
|
|
parameters.join('\n') +
|
|
'\n</Parameters>\n'
|
|
);
|
|
|
|
var properties = _(map_properties).map(function(v) { return ' ' + v; }).join('');
|
|
|
|
output.unshift(
|
|
'<?xml version="1.0" ' +
|
|
'encoding="utf-8"?>\n' +
|
|
'<!DOCTYPE Map[]>\n' +
|
|
'<Map' + properties +'>\n');
|
|
output.push('</Map>');
|
|
return output.join('\n');
|
|
};
|
|
|
|
/**
|
|
* This function currently modifies 'current'
|
|
* @param {Array} current current list of rules
|
|
* @param {Object} definition a Definition object to add to the rules
|
|
* @param {Object} byFilter an object/dictionary of existing filters. This is
|
|
* actually keyed `attachment->filter`
|
|
* @param {Object} env the current environment
|
|
*/
|
|
function addRules(current, definition, byFilter, env) {
|
|
var newFilters = definition.filters,
|
|
newRules = definition.rules,
|
|
updatedFilters, clone, previous;
|
|
|
|
// The current definition might have been split up into
|
|
// multiple definitions already.
|
|
for (var k = 0; k < current.length; k++) {
|
|
updatedFilters = current[k].filters.cloneWith(newFilters);
|
|
if (updatedFilters) {
|
|
previous = byFilter[updatedFilters];
|
|
if (previous) {
|
|
// There's already a definition with those exact
|
|
// filters. Add the current definitions' rules
|
|
// and stop processing it as the existing rule
|
|
// has already gone down the inheritance chain.
|
|
previous.addRules(newRules);
|
|
} else {
|
|
clone = current[k].clone(updatedFilters);
|
|
// Make sure that we're only maintaining the clone
|
|
// when we did actually add rules. If not, there's
|
|
// no need to keep the clone around.
|
|
if (clone.addRules(newRules)) {
|
|
// We inserted an element before this one, so we need
|
|
// to make sure that in the next loop iteration, we're
|
|
// not performing the same task for this element again,
|
|
// hence the k++.
|
|
byFilter[updatedFilters] = clone;
|
|
current.splice(k, 0, clone);
|
|
k++;
|
|
}
|
|
}
|
|
} else if (updatedFilters === null) {
|
|
// if updatedFilters is null, then adding the filters doesn't
|
|
// invalidate or split the selector, so we addRules to the
|
|
// combined selector
|
|
|
|
// Filters can be added, but they don't change the
|
|
// filters. This means we don't have to split the
|
|
// definition.
|
|
//
|
|
// this is cloned here because of shared classes, see
|
|
// sharedclass.mss
|
|
current[k] = current[k].clone();
|
|
current[k].addRules(newRules);
|
|
}
|
|
// if updatedFeatures is false, then the filters split the rule,
|
|
// so they aren't the same inheritance chain
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Apply inherited styles from their ancestors to them.
|
|
*
|
|
* called either once per render (in the case of mss) or per layer
|
|
* (for mml)
|
|
*
|
|
* @param {Object} definitions - a list of definitions objects
|
|
* that contain .rules
|
|
* @param {Object} env - the environment
|
|
* @return {Array<Array>} an array of arrays is returned,
|
|
* in which each array refers to a specific attachment
|
|
*/
|
|
function inheritDefinitions(definitions, env) {
|
|
var inheritTime = +new Date();
|
|
// definitions are ordered by specificity,
|
|
// high (index 0) to low
|
|
var byAttachment = {},
|
|
byFilter = {};
|
|
var result = [];
|
|
var current, previous, attachment;
|
|
|
|
// Evaluate the filters specified by each definition with the given
|
|
// environment to correctly resolve variable references
|
|
definitions.forEach(function(d) {
|
|
d.filters.ev(env);
|
|
});
|
|
|
|
for (var i = 0; i < definitions.length; i++) {
|
|
|
|
attachment = definitions[i].attachment;
|
|
current = [definitions[i]];
|
|
|
|
if (!byAttachment[attachment]) {
|
|
byAttachment[attachment] = [];
|
|
byAttachment[attachment].attachment = attachment;
|
|
byFilter[attachment] = {};
|
|
result.push(byAttachment[attachment]);
|
|
}
|
|
|
|
// Iterate over all subsequent rules.
|
|
for (var j = i + 1; j < definitions.length; j++) {
|
|
if (definitions[j].attachment === attachment) {
|
|
// Only inherit rules from the same attachment.
|
|
current = addRules(current, definitions[j], byFilter[attachment], env);
|
|
}
|
|
}
|
|
|
|
for (var k = 0; k < current.length; k++) {
|
|
byFilter[attachment][current[k].filters] = current[k];
|
|
byAttachment[attachment].push(current[k]);
|
|
}
|
|
}
|
|
|
|
if (env.benchmark) console.warn('Inheritance time: ' + ((new Date() - inheritTime)) + 'ms');
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
// Sort styles by the minimum index of their rules.
|
|
// This sorts a slice of the styles, so it returns a sorted
|
|
// array but does not change the input.
|
|
function sortStylesIndex(a, b) { return b.index - a.index; }
|
|
function sortStyles(styles, env) {
|
|
for (var i = 0; i < styles.length; i++) {
|
|
var style = styles[i];
|
|
style.index = Infinity;
|
|
for (var b = 0; b < style.length; b++) {
|
|
var rules = style[b].rules;
|
|
for (var r = 0; r < rules.length; r++) {
|
|
var rule = rules[r];
|
|
if (rule.index < style.index) {
|
|
style.index = rule.index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var result = styles.slice();
|
|
result.sort(sortStylesIndex);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Find a rule like Map { background-color: #fff; },
|
|
* if any, and return a list of properties to be inserted
|
|
* into the <Map element of the resulting XML. Translates
|
|
* properties of the mml object at `m` directly into XML
|
|
* properties.
|
|
*
|
|
* @param {Object} m the mml object.
|
|
* @param {Array} definitions the output of toList.
|
|
* @param {Object} env
|
|
* @return {String} rendered properties.
|
|
*/
|
|
function getMapProperties(m, definitions, env) {
|
|
var rules = {};
|
|
var symbolizers = carto.tree.Reference.data.symbolizers.map;
|
|
|
|
_(m).each(function(value, key) {
|
|
if (key in symbolizers) rules[key] = key + '="' + value + '"';
|
|
});
|
|
|
|
definitions.filter(function(r) {
|
|
return r.elements.join('') === 'Map';
|
|
}).forEach(function(r) {
|
|
for (var i = 0; i < r.rules.length; i++) {
|
|
var key = r.rules[i].name;
|
|
if (!(key in symbolizers)) {
|
|
env.error({
|
|
message: 'Rule ' + key + ' not allowed for Map.',
|
|
index: r.rules[i].index
|
|
});
|
|
}
|
|
rules[key] = r.rules[i].ev(env).toXML(env);
|
|
}
|
|
});
|
|
return rules;
|
|
}
|
|
|
|
module.exports = carto;
|
|
module.exports.addRules = addRules;
|
|
module.exports.inheritDefinitions = inheritDefinitions;
|
|
module.exports.sortStyles = sortStyles;
|