• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mapbox / carto / 663

pending completion
663

push

travis-ci

nebulon42
Merge branch 'master' of github.com:mapbox/carto

6 of 6 new or added lines in 1 file covered. (100.0%)

1606 of 1796 relevant lines covered (89.42%)

1617.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.98
/lib/carto/renderer.js
1
var _ = require('lodash');
1✔
2
var carto = require('./index');
1✔
3

4
carto.Renderer = function Renderer(env, options) {
1✔
5
    this.env = env || {};
173✔
6
    this.options = options || {};
173✔
7
    this.options.mapnik_version = this.options.mapnik_version || carto.tree.Reference.getLatest();
173✔
8
};
9

10
/**
11
 * Prepare a MSS document (given as an string) into a
12
 * XML Style fragment (mostly useful for debugging)
13
 *
14
 * @param {String} data the mss contents as a string.
15
 */
16
carto.Renderer.prototype.renderMSS = function render(data) {
1✔
17
    // effects is a container for side-effects, which currently
18
    // are limited to FontSets.
19
    var env = _(this.env).defaults({
67✔
20
        benchmark: false,
21
        validation_data: false,
22
        effects: []
23
    }).value();
24

25
    carto.tree.Reference.setVersion(this.options.mapnik_version);
67✔
26

27
    var output = [];
67✔
28
    var styles = [];
67✔
29

30
    if (env.benchmark) console.time('Parsing MSS');
67✔
31
    var parser = (carto.Parser(env)).parse(data);
67✔
32
    if (env.benchmark) console.timeEnd('Parsing MSS');
58✔
33

34
    if (env.benchmark) console.time('Rule generation');
58✔
35
    var rule_list = parser.toList(env);
58✔
36
    if (env.benchmark) console.timeEnd('Rule generation');
58✔
37

38
    if (env.benchmark) console.time('Rule inheritance');
58✔
39
    var rules = inheritDefinitions(rule_list, env);
58✔
40
    if (env.benchmark) console.timeEnd('Rule inheritance');
57✔
41

42
    if (env.benchmark) console.time('Style sort');
57✔
43
    var sorted = sortStyles(rules,env);
57✔
44
    if (env.benchmark) console.timeEnd('Style sort');
57✔
45

46
    if (env.benchmark) console.time('Total Style generation');
57✔
47
    for (var k = 0, rule, style_name; k < sorted.length; k++) {
57✔
48
        rule = sorted[k];
81✔
49
        style_name = 'style' + (rule.attachment !== '__default__' ? '-' + rule.attachment : '');
81✔
50
        styles.push(style_name);
81✔
51
        var bench_name = '\tStyle "'+style_name+'" (#'+k+') toXML';
81✔
52
        if (env.benchmark) console.time(bench_name);
81✔
53
        // env.effects can be modified by this call
54
        output.push(carto.tree.StyleXML(style_name, rule.attachment, rule, env));
81✔
55
        if (env.benchmark) console.timeEnd(bench_name);
81✔
56
    }
57
    if (env.benchmark) console.timeEnd('Total Style generation');
57✔
58
    if (env.errors) throw env.errors;
57✔
59
    return output.join('\n');
39✔
60
};
61

62
/**
63
 * Prepare a MML document (given as an object) into a
64
 * fully-localized XML file ready for Mapnik consumption
65
 *
66
 * @param {String} m - the JSON file as a string.
67
 */
68
carto.Renderer.prototype.render = function render(m) {
1✔
69
    // effects is a container for side-effects, which currently
70
    // are limited to FontSets.
71
    var env = _(this.env).defaults({
106✔
72
        benchmark: false,
73
        validation_data: false,
74
        effects: [],
75
        ppi: 90.714
76
    }).value();
77

78
    carto.tree.Reference.setVersion(this.options.mapnik_version);
106✔
79

80
    var output = [];
106✔
81
    var definitions = [];
106✔
82

83
    // Transform stylesheets into definitions.
84
    if (_.has(m, 'Stylesheet') && !_.isNil(m.Stylesheet)) {
106✔
85
        m.Stylesheet = _.castArray(m.Stylesheet);
105✔
86
        definitions = _(m.Stylesheet).chain()
105✔
87
            .map(function(s) {
88
                if (_.isString(s) || !_.has(s, 'id') || !_.has(s, 'data') || _.isNil(s.id) || _.isNil(s.data)) {
104✔
89
                    var e = new Error("Expecting a stylesheet object of the form { id: 'x', 'data': 'y' } for the Stylesheet property.\n");
2✔
90
                    e.stack = null; // do not show stack trace
2✔
91
                    throw e;
2✔
92
                }
93
                // Passing the environment from stylesheet to stylesheet,
94
                // allows frames and effects to be maintained.
95
                env = _(env).extend({filename:s.id}).value();
102✔
96

97
                var time = +new Date(),
102✔
98
                    root = (carto.Parser(env)).parse(s.data);
99
                if (env.benchmark)
98✔
100
                    console.warn('Parsing time: ' + (new Date() - time) + 'ms');
×
101
                return root.toList(env);
98✔
102
            })
103
            .flatten()
104
            .value();
105
    }
106

107
    function appliesTo(name, classIndex) {
1✔
108
        return function(definition) {
102✔
109
            return definition.appliesTo(name, classIndex);
421✔
110
        };
111
    }
112

113
    // Iterate through layers and create styles custom-built
114
    // for each of them, and apply those styles to the layers.
115
    var styles, l, classIndex, rules, sorted, matching;
100✔
116
    for (var i = 0; i < m.Layer.length; i++) {
100✔
117
        l = m.Layer[i];
108✔
118
        styles = [];
108✔
119

120
        if (_.has(l, 'name')) { // remove in 1.0.0
108✔
121
            l.layerId = l.name;
×
122
            console.warn('Warning: using the name attribute for layers (like ' + l.name + ' here) is deprecated and will be removed in 1.0.0. Use id instead.');
×
123
        }
124
        else if (_.has(l, 'id')) {
108✔
125
            l.layerId = l.id;
107✔
126
        }
127
        else {
128
            l.layerId = '';
1✔
129
            var e = new Error("Either name (deprecated) or id attributes are required for layers.\n");
1✔
130
            e.stack = null; // do not show stack trace
1✔
131
            throw e;
1✔
132
        }
133

134
        if (definitions.length > 0) {
107✔
135
            classIndex = {};
102✔
136

137
            if (env.benchmark) console.warn('processing layer: ' + l.id);
102✔
138
            // Classes are given as space-separated alphanumeric strings.
139
            var classes = (l['class'] || '').split(/\s+/g);
102✔
140
            for (var j = 0; j < classes.length; j++) {
102✔
141
                classIndex[classes[j]] = true;
123✔
142
            }
143
            matching = definitions.filter(appliesTo(l.layerId, classIndex));
102✔
144
            rules = inheritDefinitions(matching, env);
102✔
145
            sorted = sortStyles(rules, env);
101✔
146

147
            for (var k = 0, rule, style_name; k < sorted.length; k++) {
101✔
148
                rule = foldStyle(sorted[k]);
106✔
149
                style_name = l.layerId + (rule.attachment !== '__default__' ? '-' + rule.attachment : '');
106✔
150

151
                // env.effects can be modified by this call
152
                var styleXML = carto.tree.StyleXML(style_name, rule.attachment, rule, env);
106✔
153

154
                if (styleXML) {
106✔
155
                    output.push(styleXML);
104✔
156
                    styles.push(style_name);
104✔
157
                }
158
            }
159
        }
160

161
        // if there is a global _properties object for this layer take it into account
162
        if (_.has(m, '_properties') && _.has(m._properties, l.layerId)) {
106✔
163
            if (!_.has(l, 'properties')) {
2✔
164
                l.properties = {};
1✔
165
            }
166
            var props = {};
2✔
167
            _.assign(props, m._properties[l.layerId], l.properties);
2✔
168
            l.properties = props;
2✔
169
        }
170

171
        output.push(carto.tree.LayerXML(l, styles));
106✔
172
    }
173

174
    output.unshift(env.effects.map(function(e) {
98✔
175
        return e.toXML(env);
9✔
176
    }).join('\n'));
177

178
    var map_properties = getMapProperties(m, definitions, env);
98✔
179

180
    // Exit on errors.
181
    if (env.errors) throw env.errors;
98✔
182

183
    // Pass TileJSON and other custom parameters through to Mapnik XML.
184
    var parameters = _(m).reduce(function(memo, v, k) {
81✔
185
        if (!v && v !== 0) return memo;
304✔
186

187
        switch (k) {
289✔
188
        // Known skippable properties.
189
        case 'srs':
190
        case 'Layer':
191
        case 'Stylesheet':
192
            break;
239✔
193
        // Non URL-bound TileJSON properties.
194
        case 'bounds':
195
        case 'center':
196
        case 'minzoom':
197
        case 'maxzoom':
198
        case 'version':
199
            memo.push('  <Parameter name="' + k + '">' + v + '</Parameter>');
24✔
200
            break;
24✔
201
        // Properties that require CDATA.
202
        case 'name':
203
        case 'description':
204
        case 'legend':
205
        case 'attribution':
206
        case 'template':
207
            memo.push('  <Parameter name="' + k + '"><![CDATA[' + v + ']]></Parameter>');
5✔
208
            break;
5✔
209
        // Mapnik image format.
210
        case 'format':
211
            memo.push('  <Parameter name="' + k + '">' + v + '</Parameter>');
6✔
212
            break;
6✔
213
        // Mapnik interactivity settings.
214
        case 'interactivity':
215
            memo.push('  <Parameter name="interactivity_layer">' + v.layer + '</Parameter>');
1✔
216
            memo.push('  <Parameter name="interactivity_fields">' + v.fields + '</Parameter>');
1✔
217
            break;
1✔
218
        // Support any additional scalar properties.
219
        default:
220
            if ('string' === typeof v) {
14✔
221
                memo.push('  <Parameter name="' + k + '"><![CDATA[' + v + ']]></Parameter>');
2✔
222
            } else if ('number' === typeof v) {
12✔
223
                memo.push('  <Parameter name="' + k + '">' + v + '</Parameter>');
9✔
224
            } else if ('boolean' === typeof v) {
3✔
225
                memo.push('  <Parameter name="' + k + '">' + v + '</Parameter>');
1✔
226
            }
227
            break;
14✔
228
        }
229
        return memo;
289✔
230
    }, []);
231
    if (parameters.length) output.unshift(
81✔
232
        '<Parameters>\n' +
233
        parameters.join('\n') +
234
        '\n</Parameters>\n'
235
    );
236

237
    var properties = _(map_properties).map(function(v) { return ' ' + v; }).join('');
81✔
238

239
    output.unshift(
81✔
240
        '<?xml version="1.0" ' +
241
        'encoding="utf-8"?>\n' +
242
        '<!DOCTYPE Map[]>\n' +
243
        '<Map' + properties +'>\n');
244
    output.push('</Map>');
81✔
245
    return output.join('\n');
81✔
246
};
247

248
/**
249
 * This function currently modifies 'current'
250
 * @param {Array}  current  current list of rules
251
 * @param {Object} definition a Definition object to add to the rules
252
 * @param {Object} byFilter an object/dictionary of existing filters. This is
253
 * actually keyed `attachment->filter`
254
*/
255
function addRules(current, definition, byFilter, env) { // eslint-disable-line
1✔
256
    var newFilters = definition.filters,
2,733✔
257
        newRules = definition.rules,
258
        updatedFilters, clone, previous;
259

260
    // The current definition might have been split up into
261
    // multiple definitions already.
262
    for (var k = 0; k < current.length; k++) {
2,733✔
263
        updatedFilters = current[k].filters.cloneWith(newFilters);
3,812✔
264
        if (updatedFilters) {
3,812✔
265
            previous = byFilter[updatedFilters];
203✔
266
            if (previous) {
203✔
267
                // There's already a definition with those exact
268
                // filters. Add the current definitions' rules
269
                // and stop processing it as the existing rule
270
                // has already gone down the inheritance chain.
271
                previous.addRules(newRules);
7✔
272
            } else {
273
                clone = current[k].clone(updatedFilters);
196✔
274
                // Make sure that we're only maintaining the clone
275
                // when we did actually add rules. If not, there's
276
                // no need to keep the clone around.
277
                if (clone.addRules(newRules)) {
196✔
278
                    // We inserted an element before this one, so we need
279
                    // to make sure that in the next loop iteration, we're
280
                    // not performing the same task for this element again,
281
                    // hence the k++.
282
                    byFilter[updatedFilters] = clone;
129✔
283
                    current.splice(k, 0, clone);
129✔
284
                    k++;
129✔
285
                }
286
            }
287
        } else if (updatedFilters === null) {
3,609✔
288
            // if updatedFilters is null, then adding the filters doesn't
289
            // invalidate or split the selector, so we addRules to the
290
            // combined selector
291

292
            // Filters can be added, but they don't change the
293
            // filters. This means we don't have to split the
294
            // definition.
295
            //
296
            // this is cloned here because of shared classes, see
297
            // sharedclass.mss
298
            current[k] = current[k].clone();
1,837✔
299
            current[k].addRules(newRules);
1,837✔
300
        }
301
        // if updatedFeatures is false, then the filters split the rule,
302
        // so they aren't the same inheritance chain
303
    }
304
    return current;
2,733✔
305
}
306

307
/**
308
 * Apply inherited styles from their ancestors to them.
309
 *
310
 * called either once per render (in the case of mss) or per layer
311
 * (for mml)
312
 *
313
 * @param {Object} definitions - a list of definitions objects
314
 *   that contain .rules
315
 * @param {Object} env - the environment
316
 * @return {Array<Array>} an array of arrays is returned,
317
 *   in which each array refers to a specific attachment
318
 */
319
function inheritDefinitions(definitions, env) {
1✔
320
    var inheritTime = +new Date();
160✔
321
    // definitions are ordered by specificity,
322
    // high (index 0) to low
323
    var byAttachment = {},
160✔
324
        byFilter = {};
325
    var result = [];
160✔
326
    var current, attachment;
160✔
327

328
    // Evaluate the filters specified by each definition with the given
329
    // environment to correctly resolve variable references
330
    definitions.forEach(function(d) {
160✔
331
        d.filters.ev(env);
379✔
332
    });
333

334
    for (var i = 0; i < definitions.length; i++) {
158✔
335

336
        attachment = definitions[i].attachment;
377✔
337
        current = [definitions[i]];
377✔
338

339
        if (!byAttachment[attachment]) {
377✔
340
            byAttachment[attachment] = [];
187✔
341
            byAttachment[attachment].attachment = attachment;
187✔
342
            byFilter[attachment] = {};
187✔
343
            result.push(byAttachment[attachment]);
187✔
344
        }
345

346
        // Iterate over all subsequent rules.
347
        for (var j = i + 1; j < definitions.length; j++) {
377✔
348
            if (definitions[j].attachment === attachment) {
2,788✔
349
                // Only inherit rules from the same attachment.
350
                current = addRules(current, definitions[j], byFilter[attachment], env);
2,733✔
351
            }
352
        }
353

354
        for (var k = 0; k < current.length; k++) {
377✔
355
            byFilter[attachment][current[k].filters] = current[k];
506✔
356
            byAttachment[attachment].push(current[k]);
506✔
357
        }
358
    }
359

360
    if (env.benchmark) console.warn('Inheritance time: ' + ((new Date() - inheritTime)) + 'ms');
158✔
361

362
    return result;
158✔
363

364
}
365

366
// Sort styles by the minimum index of their rules.
367
// This sorts a slice of the styles, so it returns a sorted
368
// array but does not change the input.
369
function sortStylesIndex(a, b) { return b.index - a.index; }
34✔
370
function sortStyles(styles, env) { // eslint-disable-line
1✔
371
    for (var i = 0; i < styles.length; i++) {
158✔
372
        var style = styles[i];
187✔
373
        style.index = Infinity;
187✔
374
        for (var b = 0; b < style.length; b++) {
187✔
375
            var rules = style[b].rules;
506✔
376
            for (var r = 0; r < rules.length; r++) {
506✔
377
                var rule = rules[r];
4,718✔
378
                if (rule.index < style.index) {
4,718✔
379
                    style.index = rule.index;
262✔
380
                }
381
            }
382
        }
383
    }
384

385
    var result = styles.slice();
158✔
386
    result.sort(sortStylesIndex);
158✔
387
    return result;
158✔
388
}
389

390
// Removes dead style definitions that can never be reached
391
// when filter-mode="first". The style is modified in-place
392
// and returned. The style must be sorted.
393
function foldStyle(style) {
1✔
394
    for (var i = 0; i < style.length; i++) {
106✔
395
        for (var j = style.length - 1; j > i; j--) {
286✔
396
            if (style[j].filters.cloneWith(style[i].filters) === null)
10,428✔
397
                style.splice(j, 1);
118✔
398
        }
399
    }
400
    return style;
106✔
401
}
402

403
/**
404
 * Find a rule like Map { background-color: #fff; },
405
 * if any, and return a list of properties to be inserted
406
 * into the <Map element of the resulting XML. Translates
407
 * properties of the mml object at `m` directly into XML
408
 * properties.
409
 *
410
 * @param {Object} m the mml object.
411
 * @param {Array} definitions the output of toList.
412
 * @param {Object} env
413
 * @return {String} rendered properties.
414
 */
415
function getMapProperties(m, definitions, env) {
1✔
416
    var rules = {};
98✔
417
    var symbolizers = carto.tree.Reference.data.symbolizers.map;
98✔
418

419
    _(m).each(function(value, key) {
98✔
420
        if (key in symbolizers) rules[key] = key + '="' + value + '"';
354✔
421
    });
422

423
    definitions.filter(function(r) {
98✔
424
        return r.elements.join('') === 'Map';
288✔
425
    }).forEach(function(r) {
426
        for (var i = 0; i < r.rules.length; i++) {
5✔
427
            var key = r.rules[i].name;
3✔
428
            if (!(key in symbolizers)) {
3✔
429
                env.error({
×
430
                    message: 'Rule ' + key + ' not allowed for Map.',
431
                    index: r.rules[i].index
432
                });
433
            }
434
            rules[key] = r.rules[i].ev(env).toXML(env);
3✔
435
        }
436
    });
437
    return rules;
98✔
438
}
439

440
module.exports = carto;
1✔
441
module.exports.addRules = addRules;
1✔
442
module.exports.inheritDefinitions = inheritDefinitions;
1✔
443
module.exports.sortStyles = sortStyles;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc