From e919b6b27f3a60aba5f56ba331fea30015df31c5 Mon Sep 17 00:00:00 2001 From: nebulon42 Date: Fri, 2 Jun 2017 20:21:28 +0200 Subject: [PATCH] add ability to define variables within selectors and filters, ref #461 --- lib/carto/parser.js | 19 +-- lib/carto/renderer.js | 4 +- lib/carto/tree.js | 28 +++++ lib/carto/tree/call.js | 9 ++ lib/carto/tree/color.js | 9 ++ lib/carto/tree/definition.js | 67 +++++++++-- lib/carto/tree/dimension.js | 7 ++ lib/carto/tree/expression.js | 42 +++++++ lib/carto/tree/field.js | 18 +++ lib/carto/tree/filter.js | 14 +++ lib/carto/tree/filterset.js | 16 ++- lib/carto/tree/fontset.js | 28 +++++ lib/carto/tree/imagefilter.js | 36 ++++++ lib/carto/tree/invalid.js | 12 +- lib/carto/tree/keyword.js | 19 +++ lib/carto/tree/literal.js | 20 ++++ lib/carto/tree/operation.js | 39 +++++- lib/carto/tree/quoted.js | 20 ++++ lib/carto/tree/rule.js | 27 ++++- lib/carto/tree/ruleset.js | 22 +++- lib/carto/tree/selector.js | 48 +++++++- lib/carto/tree/style.js | 8 +- lib/carto/tree/url.js | 19 +++ lib/carto/tree/value.js | 44 ++++++- lib/carto/tree/variable.js | 151 ++++++++++++++++++++---- lib/carto/tree/zoom.js | 31 +++++ test/rendering/issue_461.mml | 13 ++ test/rendering/issue_461.mss | 19 +++ test/rendering/issue_461.result | 30 +++++ test/specificity/classes.result | 14 +-- test/specificity/demo.result | 16 +-- test/specificity/filters_and_ids.result | 12 +- test/specificity/issue60.result | 8 +- 33 files changed, 770 insertions(+), 99 deletions(-) create mode 100644 test/rendering/issue_461.mml create mode 100644 test/rendering/issue_461.mss create mode 100644 test/rendering/issue_461.result diff --git a/lib/carto/parser.js b/lib/carto/parser.js index 6efff1661..cd7782e7b 100644 --- a/lib/carto/parser.js +++ b/lib/carto/parser.js @@ -237,28 +237,11 @@ carto.Parser = function Parser(env) { // call populates Invalid-caused errors var definitions = this.flatten([], [], env); - definitions.sort(specificitySort); + definitions.sort(tree.specificitySort); return definitions; }; })(); - // Sort rules by specificity: this function expects selectors to be - // split already. - // - // Written to be used as a .sort(Function); - // argument. - // - // [1, 0, 0, 467] > [0, 0, 1, 520] - var specificitySort = function(a, b) { - var as = a.specificity; - var bs = b.specificity; - - if (as[0] != bs[0]) return bs[0] - as[0]; - if (as[1] != bs[1]) return bs[1] - as[1]; - if (as[2] != bs[2]) return bs[2] - as[2]; - return bs[3] - as[3]; - }; - return root; }, diff --git a/lib/carto/renderer.js b/lib/carto/renderer.js index 3473d6bad..c182a45fb 100644 --- a/lib/carto/renderer.js +++ b/lib/carto/renderer.js @@ -555,11 +555,13 @@ function sortStyles(styles) { function foldStyle(style) { for (var i = 0; i < style.length; i++) { for (var j = style.length - 1; j > i; j--) { - if (style[j].filters.cloneWith(style[i].filters) === null) { + if (style[j].zoom === style[i].zoom && + style[j].filters.cloneWith(style[i].filters) === null) { style.splice(j, 1); } } } + return style; } diff --git a/lib/carto/tree.js b/lib/carto/tree.js index a8d6db986..02db67535 100644 --- a/lib/carto/tree.js +++ b/lib/carto/tree.js @@ -8,3 +8,31 @@ module.exports.find = function (obj, fun) { } return null; }; + +// Sort rules by specificity: this function expects selectors to be +// split already. +// +// Specificity: [ID, Class, Filters, Zoom, Position in document] +// +// Written to be used as a .sort(Function); +// argument. +// +// [1, 0, 0, 467] > [0, 0, 1, 520] +module.exports.specificitySort = function (a, b) { + var as = a.specificity; + var bs = b.specificity; + + if (as[0] != bs[0]) { // ID + return bs[0] - as[0]; + } + if (as[1] != bs[1]) { // Class + return bs[1] - as[1]; + } + if (as[2] != bs[2]) { // Filters + return bs[2] - as[2]; + } + if (as[3] != -1 && bs[3] != -1 && as[3] != bs[3]) { // Zoom + return bs[4] - as[4]; + } + return bs[4] - as[4]; // Position in document +}; diff --git a/lib/carto/tree/call.js b/lib/carto/tree/call.js index eb530168f..ff44cb8c4 100644 --- a/lib/carto/tree/call.js +++ b/lib/carto/tree/call.js @@ -7,6 +7,7 @@ tree.Call = function Call(name, args, filename, index) { this.args = args; this.filename = filename; this.index = index; + this.rule = null; }; tree.Call.prototype = { @@ -105,6 +106,14 @@ tree.Call.prototype = { } else { return this.name; } + }, + setRule: function (rule) { + this.rule = rule; + _.forEach(this.args, function (a) { + if (typeof a.setRule === 'function') { + a.setRule(rule); + } + }); } }; diff --git a/lib/carto/tree/color.js b/lib/carto/tree/color.js index 8165a3557..7cab91145 100644 --- a/lib/carto/tree/color.js +++ b/lib/carto/tree/color.js @@ -29,6 +29,7 @@ tree.Color = function Color(hsl, a, perceptual) { } else { this.perceptual = false; } + this.rule = null; }; tree.Color.prototype = { @@ -149,6 +150,14 @@ tree.Color.prototype = { round: function(value, decimals) { return Number(Math.round(value+'e'+decimals)+'e-'+decimals); + }, + + setRule: function (rule) { + this.rule = rule; + }, + + clone: function () { + return _.clone(this); } }; diff --git a/lib/carto/tree/definition.js b/lib/carto/tree/definition.js index ecc793750..14693f06e 100644 --- a/lib/carto/tree/definition.js +++ b/lib/carto/tree/definition.js @@ -9,7 +9,9 @@ var assert = require('assert'), // } // // The selector can have filters -tree.Definition = function Definition(selector, rules) { +tree.Definition = function Definition(selector, rules, env) { + var that = this; + this.elements = selector.elements; assert.ok(selector.filters instanceof tree.Filterset); this.rules = rules; @@ -19,10 +21,42 @@ tree.Definition = function Definition(selector, rules) { this.rules[i].zoom = selector.zoom; this.ruleIndex[this.rules[i].updateID()] = true; } + this.filters = selector.filters; + if (this.filters) { + if (_.isArray(this.filters)) { + _.forEach(this.filters, function (f) { + if (typeof f.setRule === 'function') { + f.setRule(that); + } + }); + } + else { + if (typeof this.filters.setRule === 'function') { + this.filters.setRule(that); + } + } + } + this.zoom = selector.zoom; + + if (this.zoom) { + if (_.isArray(this.zoom)) { + _.forEach(this.zoom, function (f) { + if (typeof f.setRule === 'function') { + f.setRule(that); + } + }); + } + else { + if (typeof this.zoom.setRule === 'function') { + this.zoom.setRule(that); + } + } + } + this.attachment = selector.attachment || '__default__'; - this.specificity = selector.specificity(); + this.specificity = selector.specificity(env); this.matchCount = 0; // special handling for Map selector @@ -47,17 +81,34 @@ tree.Definition.prototype.clone = function(filters) { clone.ruleIndex = _.clone(this.ruleIndex); clone.filters = filters ? filters : this.filters.clone(); clone.attachment = this.attachment; + clone.matchCount = this.matchCount; + clone.specificity = this.specificity; + clone.zoom = this.zoom; return clone; }; tree.Definition.prototype.addRules = function(rules) { - var added = 0; + var added = 0, + parent = null; + + if (this.rules.length && this.rules[0]) { + parent = this.rules[0].parent; + } // Add only unique rules. for (var i = 0; i < rules.length; i++) { - if (!this.ruleIndex[rules[i].id]) { - this.rules.push(rules[i]); - this.ruleIndex[rules[i].id] = true; + var rule = rules[i].clone(); + rule.parent = parent; + // match rule to definition zoom level + // if rule zoom less specific than definition zoom + if (this.zoom && rule.zoom > this.zoom) { + rule.zoom = this.zoom; + rule.updateID(); + } + + if (!this.ruleIndex[rule.id]) { + this.rules.push(rule); + this.ruleIndex[rule.id] = true; added++; } } @@ -224,9 +275,9 @@ tree.Definition.prototype.collectSymbolizers = function(zooms, i) { } }; -// The tree.Zoom.toString function ignores the holes in zoom ranges and outputs +// The tree.Zoom.toObject function ignores the holes in zoom ranges and outputs // scaledenominators that cover the whole range from the first to last bit set. -// This algorithm can produces zoom ranges that may have holes. However, +// This algorithm can produce zoom ranges that may have holes. However, // 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. diff --git a/lib/carto/tree/dimension.js b/lib/carto/tree/dimension.js index 4d1e70707..dcd0cad80 100644 --- a/lib/carto/tree/dimension.js +++ b/lib/carto/tree/dimension.js @@ -10,6 +10,7 @@ tree.Dimension = function Dimension(value, unit, index, filename) { this.unit = unit || null; this.filename = filename; this.index = index; + this.rule = null; }; tree.Dimension.prototype = { @@ -96,6 +97,12 @@ tree.Dimension.prototype = { //here the operands are either the same (% or undefined or px), or one is undefined and the other is px return new tree.Dimension(tree.operate(op, this.value, other.value), this.unit || other.unit, this.index, this.filename); + }, + setRule: function (rule) { + this.rule = rule; + }, + clone: function () { + return _.clone(this); } }; diff --git a/lib/carto/tree/expression.js b/lib/carto/tree/expression.js index dfe1da038..7c3a6138b 100644 --- a/lib/carto/tree/expression.js +++ b/lib/carto/tree/expression.js @@ -4,6 +4,7 @@ var _ = require('lodash'); tree.Expression = function Expression(value) { this.value = value; + this.rule = null; }; tree.Expression.prototype = { @@ -33,6 +34,47 @@ tree.Expression.prototype = { return mappedVal; } return mappedVal.join(' '); + }, + + setRule: function (rule) { + this.rule = rule; + if (_.isArray(this.value)) { + _.forEach(this.value, function (v) { + if (typeof v.setRule === 'function') { + v.setRule(rule); + } + }); + } + else { + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } + } + }, + + clone: function () { + var clone = Object.create(tree.Expression.prototype); + clone.rule = this.rule; + if (_.isArray(this.value)) { + clone.value = []; + _.forEach(this.value, function (v) { + if (typeof v.clone === 'function') { + clone.value.push(v.clone()); + } + else { + clone.value.push(v); + } + }); + } + else { + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } + } + return clone; } }; diff --git a/lib/carto/tree/field.js b/lib/carto/tree/field.js index a98b904e1..9a47733fc 100644 --- a/lib/carto/tree/field.js +++ b/lib/carto/tree/field.js @@ -2,6 +2,7 @@ tree.Field = function Field(content) { this.value = content || ''; + this.rule = null; }; tree.Field.prototype = { @@ -11,6 +12,23 @@ tree.Field.prototype = { }, 'ev': function() { return this; + }, + setRule: function (rule) { + this.rule = rule; + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } + }, + clone: function () { + var clone = Object.create(tree.Field.prototype); + clone.rule = this.rule; + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } + return clone; } }; diff --git a/lib/carto/tree/filter.js b/lib/carto/tree/filter.js index 59dd2d562..5165a60a0 100644 --- a/lib/carto/tree/filter.js +++ b/lib/carto/tree/filter.js @@ -8,6 +8,7 @@ tree.Filter = function Filter(key, op, val, index, filename) { this.val = val; this.index = index; this.filename = filename; + this.rule = null; this.id = this.key + this.op + this.val; }; @@ -67,4 +68,17 @@ tree.Filter.prototype.toString = function() { return '[' + this.id + ']'; }; +tree.Filter.prototype.setRule = function (rule) { + this.rule = rule; + if (typeof this.key.setRule === 'function') { + this.key.setRule(rule); + } + if (typeof this.op.setRule === 'function') { + this.op.setRule(rule); + } + if (typeof this.val.setRule === 'function') { + this.val.setRule(rule); + } +} + })(require('../tree')); diff --git a/lib/carto/tree/filterset.js b/lib/carto/tree/filterset.js index a72ebc307..412a64854 100644 --- a/lib/carto/tree/filterset.js +++ b/lib/carto/tree/filterset.js @@ -1,8 +1,10 @@ var tree = require('../tree'), - util = require('../util'); + util = require('../util'), + _ = require('lodash'); tree.Filterset = function Filterset() { this.filters = {}; + this.rule = null; }; tree.Filterset.prototype.toObject = function(env) { @@ -50,8 +52,9 @@ tree.Filterset.prototype.ev = function(env) { tree.Filterset.prototype.clone = function() { var clone = new tree.Filterset(); for (var id in this.filters) { - clone.filters[id] = this.filters[id]; + clone.filters[id] = _.clone(this.filters[id]); } + clone.rule = this.rule; return clone; }; @@ -288,3 +291,12 @@ tree.Filterset.prototype.add = function(filter) { } } }; + +tree.Filterset.prototype.setRule = function (rule) { + this.rule = rule; + _.forEach(this.filters, function (f) { + if (typeof f.setRule === 'function') { + f.setRule(rule); + } + }); +} diff --git a/lib/carto/tree/fontset.js b/lib/carto/tree/fontset.js index 6c530a8b5..61ec888d7 100644 --- a/lib/carto/tree/fontset.js +++ b/lib/carto/tree/fontset.js @@ -1,5 +1,7 @@ (function(tree) { +var _ = require('lodash'); + tree._flattenFontArray = function (fonts) { var result = []; @@ -29,6 +31,7 @@ tree._getFontSet = function(env, fonts) { tree.FontSet = function FontSet(env, fonts) { this.fonts = fonts; this.name = 'fontset-' + env.effects.length; + this.rule = null; }; tree.FontSet.prototype.toObject = function() { @@ -48,4 +51,29 @@ tree.FontSet.prototype.toObject = function() { }; }; +tree.FontSet.prototype.setRule = function (rule) { + this.rule = rule; + _.forEach(this.fonts, function (f) { + if (typeof f.setRule === 'function') { + f.setRule(rule); + } + }); +}; + +tree.FontSet.prototype.clone = function () { + var clone = Object.create(tree.FontSet.prototype); + clone.rule = this.rule; + clone.name = this.name; + clone.fonts = []; + _.forEach(this.fonts, function (f) { + if (typeof f.clone === 'function') { + clone.fonts.push(f.clone()); + } + else { + clone.fonts.push(f); + } + }); + return clone; +}; + })(require('../tree')); diff --git a/lib/carto/tree/imagefilter.js b/lib/carto/tree/imagefilter.js index 99a1fe5ac..d1e2ba0df 100644 --- a/lib/carto/tree/imagefilter.js +++ b/lib/carto/tree/imagefilter.js @@ -1,8 +1,11 @@ (function(tree) { +var _ = require('lodash'); + tree.ImageFilter = function ImageFilter(filter, args) { this.filter = filter; this.args = args || null; + this.rule = null; }; tree.ImageFilter.prototype = { @@ -18,5 +21,38 @@ tree.ImageFilter.prototype = { } }; +tree.ImageFilter.prototype.setRule = function (rule) { + this.rule = rule; + if (typeof this.filter.setRule === 'function') { + this.filter.setRule(rule); + } + _.forEach(this.args, function (a) { + if (typeof a.setRule === 'function') { + a.setRule(rule); + } + }); +}; + +tree.ImageFilter.prototype.clone = function () { + var clone = Object.create(tree.ImageFilter.prototype); + clone.rule = this.rule; + if (typeof this.filter.clone === 'function') { + clone.filter = this.filter.clone(); + } + else { + clone.filter = this.filter; + } + clone.args = []; + _.forEach(this.args, function (a) { + if (typeof a.clone === 'function') { + clone.args.push(a.clone()); + } + else { + clone.args.push(a); + } + }); + return clone; +}; + })(require('../tree')); diff --git a/lib/carto/tree/invalid.js b/lib/carto/tree/invalid.js index ad37a7f61..aef156f1e 100644 --- a/lib/carto/tree/invalid.js +++ b/lib/carto/tree/invalid.js @@ -1,12 +1,14 @@ (function (tree) { -var util = require('../util'); +var util = require('../util'), + _ = require('lodash'); tree.Invalid = function Invalid(chunk, index, message, filename) { this.chunk = chunk; this.index = index; this.message = message || "Invalid code: " + this.chunk; this.filename = filename; + this.rule = null; }; tree.Invalid.prototype.is = 'invalid'; @@ -22,4 +24,12 @@ tree.Invalid.prototype.ev = function(env) { is: 'undefined' }; }; + +tree.Invalid.prototype.setRule = function (rule) { + this.rule = rule; +}; + +tree.Invalid.prototype.clone = function () { + return _.clone(this); +}; })(require('../tree')); diff --git a/lib/carto/tree/keyword.js b/lib/carto/tree/keyword.js index ed317717d..5926e833f 100644 --- a/lib/carto/tree/keyword.js +++ b/lib/carto/tree/keyword.js @@ -8,10 +8,29 @@ tree.Keyword = function Keyword(value) { 'false': 'boolean' }; this.is = special[value] ? special[value] : 'keyword'; + this.rule = null; }; tree.Keyword.prototype = { ev: function() { return this; }, toString: function() { return this.value; } }; +tree.Keyword.prototype.setRule = function (rule) { + this.rule = rule; + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } +}; +tree.Keyword.prototype.clone = function () { + var clone = Object.create(tree.Keyword.prototype); + clone.is = this.is; + clone.rule = this.rule; + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } + return clone; +}; })(require('../tree')); diff --git a/lib/carto/tree/literal.js b/lib/carto/tree/literal.js index 5fb6968e4..9952f9110 100644 --- a/lib/carto/tree/literal.js +++ b/lib/carto/tree/literal.js @@ -6,6 +6,7 @@ tree.Literal = function Field(content) { this.value = content || ''; this.is = 'field'; + this.rule = null; }; tree.Literal.prototype = { @@ -17,4 +18,23 @@ tree.Literal.prototype = { } }; +tree.Literal.prototype.setRule = function (rule) { + this.rule = rule; + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } +}; + +tree.Literal.prototype.clone = function () { + var clone = Object.create(tree.Literal.prototype); + clone.is = this.is; + clone.rule = this.rule; + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } +}; + })(require('../tree')); diff --git a/lib/carto/tree/operation.js b/lib/carto/tree/operation.js index 473d785e7..71039450e 100644 --- a/lib/carto/tree/operation.js +++ b/lib/carto/tree/operation.js @@ -2,13 +2,15 @@ // like 2 + 1. (function(tree) { -var util = require('../util'); +var util = require('../util'), + _ = require('lodash'); tree.Operation = function Operation(op, operands, index, filename) { this.op = op.trim(); this.operands = operands; this.index = index; this.filename = filename; + this.rule = null; }; tree.Operation.prototype.is = 'operation'; @@ -89,6 +91,41 @@ tree.Operation.prototype.toString = function () { return this.operands[0].toString() + this.op + this.operands[1].toString(); }; +tree.Operation.prototype.setRule = function (rule) { + this.rule = rule; + if (typeof this.op.setRule === 'function') { + this.op.setRule(rule); + } + _.forEach(this.operands, function (o) { + if (typeof o.setRule === 'function') { + o.setRule(rule); + } + }); +}; + +tree.Operation.prototype.clone = function () { + var clone = Object.create(tree.Operation.prototype); + clone.index = this.index; + clone.rule = this.rule; + clone.filename = this.filename; + if (typeof this.op.clone === 'function') { + clone.op = this.op.clone(); + } + else { + clone.op = this.op; + } + clone.operands = []; + _.forEach(this.operands, function (o) { + if (typeof o.clone === 'function') { + clone.operands.push(o.clone()); + } + else { + clone.operands.push(o); + } + }); + return clone; +}; + tree.operate = function(op, a, b) { switch (op) { case '+': return a + b; diff --git a/lib/carto/tree/quoted.js b/lib/carto/tree/quoted.js index 0d3045e1e..e1d092338 100644 --- a/lib/carto/tree/quoted.js +++ b/lib/carto/tree/quoted.js @@ -2,6 +2,7 @@ tree.Quoted = function Quoted(content) { this.value = content || ''; + this.rule = null; }; tree.Quoted.prototype = { @@ -24,6 +25,25 @@ tree.Quoted.prototype = { operate: function(env, op, other) { return new tree.Quoted(tree.operate(op, this.toString(), other.toString(this.contains_field))); + }, + + setRule: function (rule) { + this.rule = rule; + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } + }, + + clone: function () { + var clone = Object.create(tree.Quoted.prototype); + clone.rule = this.rule; + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } + return clone; } }; diff --git a/lib/carto/tree/rule.js b/lib/carto/tree/rule.js index 393b67e8b..25d74af4a 100644 --- a/lib/carto/tree/rule.js +++ b/lib/carto/tree/rule.js @@ -12,10 +12,23 @@ tree.Rule = function Rule(name, value, index, filename) { this.instance = parts.length ? parts[0] : '__default__'; this.value = (value instanceof tree.Value) ? value : new tree.Value([value]); + + if (typeof this.value.setRule === 'function') { + this.value.setRule(this); + } + this.index = index; - this.symbolizer = tree.Reference.symbolizer(this.name); this.filename = filename; this.variable = (name.charAt(0) === '@'); + + if (this.variable) { + this.symbolizer = '*'; + } + else { + this.symbolizer = tree.Reference.symbolizer(this.name); + } + + this.parent = null; }; tree.Rule.prototype.is = 'rule'; @@ -23,12 +36,16 @@ tree.Rule.prototype.is = 'rule'; tree.Rule.prototype.clone = function() { var clone = Object.create(tree.Rule.prototype); clone.name = this.name; - clone.value = this.value; + clone.value = this.value.clone(); + clone.value.setRule(clone); clone.index = this.index; clone.instance = this.instance; clone.symbolizer = this.symbolizer; clone.filename = this.filename; clone.variable = this.variable; + clone.parent = this.parent; + clone.id = this.id; + clone.zoom = this.zoom; return clone; }; @@ -43,7 +60,7 @@ tree.Rule.prototype.toString = function() { tree.Rule.prototype.validate = function (env) { var valid = true; - if (!tree.Reference.validSelector(this.name)) { + if (!tree.Reference.validSelector(this.name) && !this.variable) { var mean = getMean(this.name); var mean_message = ''; if (mean[0][1] < 3) { @@ -84,7 +101,7 @@ tree.Rule.prototype.validate = function (env) { if ((this.value instanceof tree.Value) && !tree.Reference.validValue(env, this.name, this.value)) { - if (!tree.Reference.selector(this.name)) { + if (!tree.Reference.selector(this.name) && !this.variable) { valid = false; util.error(env, { message: 'Unrecognized property: ' + @@ -92,7 +109,7 @@ tree.Rule.prototype.validate = function (env) { index: this.index, filename: this.filename }); - } else { + } else if (!this.variable) { var typename; if (tree.Reference.selector(this.name).validate) { typename = tree.Reference.selector(this.name).validate; diff --git a/lib/carto/tree/ruleset.js b/lib/carto/tree/ruleset.js index 33f4f4316..b0f4a4bc4 100644 --- a/lib/carto/tree/ruleset.js +++ b/lib/carto/tree/ruleset.js @@ -3,8 +3,19 @@ var util = require('../util'); tree.Ruleset = function Ruleset(selectors, rules) { + var that = this; + this.selectors = selectors; this.rules = rules; + this.rules.forEach(function (r) { + if (r instanceof tree.Ruleset) { + r.parent = that; + } + if (r instanceof tree.Rule) { + r.parent = that; + } + }); + this.parent = null; // static cache of find() function this._lookups = {}; }; @@ -93,9 +104,12 @@ tree.Ruleset.prototype = { }, flatten: function(result, parents, env) { var selectors = [], i, j; - if (this.selectors.length === 0) { - env.frames = env.frames.concat(this.rules); - } + // add variables to env.frames + this.rules.forEach(function (r) { + if (r.variable) { + env.frames.push(r); + } + }); // evaluate zoom variables on this object. this.evZooms(env); for (i = 0; i < this.selectors.length; i++) { @@ -169,7 +183,7 @@ tree.Ruleset.prototype = { if (index !== false) { selectors[i].index = index; } - result.push(new tree.Definition(selectors[i], rules.slice())); + result.push(new tree.Definition(selectors[i], rules.slice(), env)); } return result; diff --git a/lib/carto/tree/selector.js b/lib/carto/tree/selector.js index 76fb5daa3..843bd5508 100644 --- a/lib/carto/tree/selector.js +++ b/lib/carto/tree/selector.js @@ -1,26 +1,66 @@ (function(tree) { +var _ = require('lodash'); + tree.Selector = function Selector(filters, zoom, elements, attachment, conditions, index) { + var that = this; + this.elements = elements || []; this.attachment = attachment; this.filters = filters || {}; this.zoom = typeof zoom !== 'undefined' ? zoom : tree.Zoom.all; this.conditions = conditions; this.index = index; + + if (this.filters) { + if (_.isArray(this.filters)) { + _.forEach(this.filters, function (f) { + if (typeof f.setRule === 'function') { + f.setRule(that); + } + }); + } + else { + if (typeof this.filters.setRule === 'function') { + this.filters.setRule(that); + } + } + } + + if (this.zoom) { + if (_.isArray(this.zoom)) { + _.forEach(this.zoom, function (f) { + if (typeof f.setRule === 'function') { + f.setRule(that); + } + }); + } + else { + if (typeof this.zoom.setRule === 'function') { + this.zoom.setRule(that); + } + } + } }; // Determine the specificity of this selector // based on the specificity of its elements - calling // Element.specificity() in order to do so // -// [ID, Class, Filters, Position in document] -tree.Selector.prototype.specificity = function() { +// [ID, Class, Filters, Zoom, Position in document] +tree.Selector.prototype.specificity = function(env) { + var zoomVal = -1; + // if we would evaluate zooms here we would enter an infinite loop + if (!_.isArray(this.zoom)) { + zoomVal = this.zoom; + } + return this.elements.reduce(function(memo, e) { - var spec = e.specificity(); + var spec = e.specificity(env); memo[0] += spec[0]; memo[1] += spec[1]; return memo; - }, [0, 0, this.conditions, this.index]); + }, [0, 0, this.conditions, zoomVal, this.index]); }; })(require('../tree')); diff --git a/lib/carto/tree/style.js b/lib/carto/tree/style.js index ce110ddb3..7df2adfdb 100644 --- a/lib/carto/tree/style.js +++ b/lib/carto/tree/style.js @@ -52,11 +52,11 @@ tree.StyleObject = function(name, attachment, definitions, env) { var styleAttrs = {}; if (image_filters.length) { - _.set(styleAttrs, 'image-filters', _.chain(image_filters) + _.set(styleAttrs, 'image-filters', _.uniq(_.chain(image_filters) // prevent identical filters from being duplicated in the style .uniq(function(i) { return i.id; }).map(function(f) { return f.ev(env).toObject(env, true, ',', 'image-filter'); - }).value().join(',')); + }).value()).join(',')); } if (image_filters_inflate.length) { @@ -64,11 +64,11 @@ tree.StyleObject = function(name, attachment, definitions, env) { } if (direct_image_filters.length) { - _.set(styleAttrs, 'direct-image-filters', _.chain(direct_image_filters) + _.set(styleAttrs, 'direct-image-filters', _.uniq(_.chain(direct_image_filters) // prevent identical filters from being duplicated in the style .uniq(function(i) { return i.id; }).map(function(f) { return f.ev(env).toObject(env, true, ',', 'direct-image-filter'); - }).value().join(',')); + }).value()).join(',')); } if (comp_op.length && comp_op[0].value.ev(env).value != 'src-over') { diff --git a/lib/carto/tree/url.js b/lib/carto/tree/url.js index fc8fdb740..5c7cefef5 100644 --- a/lib/carto/tree/url.js +++ b/lib/carto/tree/url.js @@ -3,6 +3,7 @@ tree.URL = function URL(val, paths) { this.value = val; this.paths = paths; + this.rule = null; }; tree.URL.prototype = { @@ -12,6 +13,24 @@ tree.URL.prototype = { }, ev: function(ctx) { return new tree.URL(this.value.ev(ctx), this.paths); + }, + setRule: function (rule) { + this.rule = rule; + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } + }, + clone: function () { + var clone = Object.create(tree.URL.prototype); + clone.rule = this.rule; + clone.paths = this.paths; + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } + return clone; } }; diff --git a/lib/carto/tree/value.js b/lib/carto/tree/value.js index 4ecbbfeee..64d291c84 100644 --- a/lib/carto/tree/value.js +++ b/lib/carto/tree/value.js @@ -4,6 +4,7 @@ var _ = require('lodash'); tree.Value = function Value(value) { this.value = value; + this.rule = null; }; tree.Value.prototype = { @@ -34,11 +35,44 @@ tree.Value.prototype = { return mappedVal.join(sep || ', '); }, clone: function() { - var obj = Object.create(tree.Value.prototype); - if (Array.isArray(obj)) obj.value = this.value.slice(); - else obj.value = this.value; - obj.is = this.is; - return obj; + var clone = Object.create(tree.Value.prototype); + if (_.isArray(this.value)) { + clone.value = []; + _.forEach(this.value, function (v) { + if (typeof v.clone === 'function') { + clone.value.push(v.clone()); + } + else { + clone.value.push(v); + } + }); + } + else { + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } + } + clone.is = this.is; + clone.rule = this.rule; + return clone; + }, + setRule: function (rule) { + this.rule = rule; + if (_.isArray(this.value)) { + _.forEach(this.value, function (v) { + if (typeof v.setRule === 'function') { + v.setRule(rule); + } + }); + } + else { + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } + } } }; diff --git a/lib/carto/tree/variable.js b/lib/carto/tree/variable.js index abacb820e..019d4c5d0 100644 --- a/lib/carto/tree/variable.js +++ b/lib/carto/tree/variable.js @@ -1,38 +1,147 @@ (function(tree) { -var util = require('../util'); + var util = require('../util'), + _ = require('lodash'); -tree.Variable = function Variable(name, index, filename) { - this.name = name; - this.index = index; - this.filename = filename; -}; + var findSelector = function (ruleset) { + if (ruleset.selectors.length && ruleset.selectors[0].elements.length) { + return ruleset.selectors[0]; + } + else { + if (ruleset.parent) { + return findSelector(ruleset.parent); + } + return null; + } + }; + + var findFilter = function (ruleset) { + if (ruleset.selectors.length && ruleset.selectors[0].filters) { + return ruleset.selectors[0].filters; + } + else { + if (ruleset.parent) { + return findFilter(ruleset.parent); + } + return null; + } + }; -tree.Variable.prototype = { - is: 'variable', - toString: function() { + tree.Variable = function Variable(name, index, filename) { + this.name = name; + this.index = index; + this.filename = filename; + this.rule = null; + }; + + tree.Variable.prototype.is = 'variable'; + + tree.Variable.prototype.toString = function() { return this.name; - }, - ev: function(env) { + }; + + tree.Variable.prototype.ev = function (env) { + var that = this, + variableDefs = [], + ruleSpecificity = null; + if (this._css) return this._css; - var thisframe = env.frames.filter(function(f) { - return f.name == this.name; - }.bind(this)); - if (thisframe.length) { - return thisframe[thisframe.length - 1].value.ev(env); + _.forEach(_.filter(env.frames, function (f) { + return f.name == that.name; + }), function (f) { + var selector = findSelector(f.parent), + specificity = [0, 0, 0, tree.Zoom.all, f.index], + parentSelectorSpecificity = null; + + if (f.parent.selectors.length) { + parentSelectorSpecificity = f.parent.selectors[0].specificity(env); + } + + if (selector) { + selector = selector.elements; + specificity = selector[0].specificity(env); + if (specificity.length == 2 && parentSelectorSpecificity) { + specificity = [ + specificity[0], + specificity[1], + parentSelectorSpecificity[2], + parentSelectorSpecificity[3], + parentSelectorSpecificity[4] + ]; + } + } + + variableDefs.push({ + selector: selector, + filter: findFilter(f.parent), + specificity: specificity, + rule: f + }); + }); + if (variableDefs.length) { + // only bother with complex evaluations if there is more than one alternative + if (variableDefs.length > 1) { + variableDefs.sort(tree.specificitySort); + var rSpec = null; + + if (that.rule instanceof tree.Selector || + that.rule instanceof tree.Definition) { + if (typeof that.rule.specificity === 'function') { + rSpec = that.rule.specificity(env); + } + else { + rSpec = that.rule.specificity; + } + + ruleSpecificity = { + specificity: rSpec + }; + } + else { + if (that.rule.parent && that.rule.parent.selectors.length) { + var pSpec = findSelector(that.rule.parent).specificity(env); + + rSpec = that.rule.parent.selectors[0].specificity(env); + ruleSpecificity = { + specificity: [pSpec[0], pSpec[1], rSpec[2], rSpec[3], rSpec[4]] + }; + } + else { + ruleSpecificity = { + specificity: [0, 0, 0, tree.Zoom.all, that.rule.index] + }; + } + } + + for (var i = 0; i < variableDefs.length && + tree.specificitySort(variableDefs[i], ruleSpecificity) < 0; i++); + i = Math.min(i, variableDefs.length - 1); + + return variableDefs[i].rule.value.ev(env); + } + else { + return variableDefs[0].rule.value.ev(env); + } } else { util.error(env, { - message: 'variable ' + this.name + ' is undefined', - index: this.index, - filename: this.filename + message: 'variable ' + that.name + ' is undefined', + index: that.index, + filename: that.filename }); return { is: 'undefined', value: 'undefined' }; } - } -}; + }; + + tree.Variable.prototype.setRule = function (rule) { + this.rule = rule; + }; + + tree.Variable.prototype.clone = function () { + return _.clone(this); + }; })(require('../tree')); diff --git a/lib/carto/tree/zoom.js b/lib/carto/tree/zoom.js index 50b39c20c..5f844cc1d 100644 --- a/lib/carto/tree/zoom.js +++ b/lib/carto/tree/zoom.js @@ -9,6 +9,7 @@ tree.Zoom = function(op, value, index, filename) { this.value = value; this.index = index; this.filename = filename; + this.rule = null; }; tree.Zoom.prototype.setZoom = function(zoom) { @@ -126,3 +127,33 @@ tree.Zoom.prototype.toString = function() { } return str; }; + +tree.Zoom.prototype.setRule = function (rule) { + this.rule = rule; + if (typeof this.op.setRule === 'function') { + this.op.setRule(rule); + } + if (typeof this.value.setRule === 'function') { + this.value.setRule(rule); + } +}; + +tree.Zoom.prototype.clone = function () { + var clone = Object.create(tree.Zoom.prototype); + clone.index = this.index; + clone.filename = this.filename; + clone.rule = this.rule; + if (typeof this.op.clone === 'function') { + clone.op = this.op.clone(); + } + else { + clone.op = this.op; + } + if (typeof this.value.clone === 'function') { + clone.value = this.value.clone(); + } + else { + clone.value = this.value; + } + return clone; +}; diff --git a/test/rendering/issue_461.mml b/test/rendering/issue_461.mml new file mode 100644 index 000000000..4f759a20e --- /dev/null +++ b/test/rendering/issue_461.mml @@ -0,0 +1,13 @@ +{ + "Stylesheet": [ + "issue_461.mss" + ], + "Layer": [ + { + "id": "world" + }, + { + "id": "sea" + } + ] +} diff --git a/test/rendering/issue_461.mss b/test/rendering/issue_461.mss new file mode 100644 index 000000000..478279802 --- /dev/null +++ b/test/rendering/issue_461.mss @@ -0,0 +1,19 @@ +@var: 'foo'; + +#world { + @var: 'oof'; + [zoom >= 5] { + @var: 'roof'; + } + text-face-name: 'Arial'; + text-name: @var + 'bar'; +} + +#sea { + @var: 'oof'; + [test = 5] { + @var: 'roof'; + } + text-face-name: 'Arial'; + text-name: @var + 'bar'; +} diff --git a/test/rendering/issue_461.result b/test/rendering/issue_461.result new file mode 100644 index 000000000..b856e0a04 --- /dev/null +++ b/test/rendering/issue_461.result @@ -0,0 +1,30 @@ + + + + + + world + + + + sea + + + diff --git a/test/specificity/classes.result b/test/specificity/classes.result index 0e41a46bc..7dcf85f1a 100644 --- a/test/specificity/classes.result +++ b/test/specificity/classes.result @@ -1,9 +1,9 @@ [ - {"elements":[".foo",".bar",".baz"],"specificity":[0,3,0,29],"matchCount": 0}, - {"elements":[".foo",".baz"],"specificity":[0,2,0,68],"matchCount": 0}, - {"elements":[".baz",".foo"],"specificity":[0,2,0,55],"matchCount": 0}, - {"elements":[".baz",".bar"],"specificity":[0,2,0,0],"matchCount": 0}, - {"elements":[".baz"],"specificity":[0,1,0,47],"matchCount": 0}, - {"elements":[".bar"],"specificity":[0,1,0,21],"matchCount": 0}, - {"elements":[".foo"],"specificity":[0,1,0,13],"matchCount": 0} + {"elements":[".foo",".bar",".baz"],"specificity":[0,3,0,67108863,29],"matchCount": 0}, + {"elements":[".foo",".baz"],"specificity":[0,2,0,67108863,68],"matchCount": 0}, + {"elements":[".baz",".foo"],"specificity":[0,2,0,67108863,55],"matchCount": 0}, + {"elements":[".baz",".bar"],"specificity":[0,2,0,67108863,0],"matchCount": 0}, + {"elements":[".baz"],"specificity":[0,1,0,67108863,47],"matchCount": 0}, + {"elements":[".bar"],"specificity":[0,1,0,67108863,21],"matchCount": 0}, + {"elements":[".foo"],"specificity":[0,1,0,67108863,13],"matchCount": 0} ] diff --git a/test/specificity/demo.result b/test/specificity/demo.result index a36e3ed3b..5c755286b 100644 --- a/test/specificity/demo.result +++ b/test/specificity/demo.result @@ -1,10 +1,10 @@ [ - {"elements":["#countries",".foo",".bar",".baz"],"specificity":[1,3,0,241],"matchCount": 0}, - {"elements":["#countries",".countries",".two"],"specificity":[1,2,0,149],"matchCount": 0}, - {"elements":["#world"],"filters":["[NAME]=United States","[BLUE]=red"],"specificity":[1,0,2,90],"matchCount": 0}, - {"elements":["#world"],"filters":["[NAME]=United States"],"specificity":[1,0,1,66],"matchCount": 0}, - {"elements":["#countries"],"specificity":[1,0,0,241],"matchCount": 0}, - {"elements":["#countries"],"specificity":[1,0,0,194],"matchCount": 0}, - {"elements":["#world"],"specificity":[1,0,0,194],"matchCount": 0}, - {"elements":["#world"],"specificity":[1,0,0,11],"matchCount": 0} + {"elements":["#countries",".foo",".bar",".baz"],"specificity":[1,3,0,67108863,241],"matchCount": 0}, + {"elements":["#countries",".countries",".two"],"specificity":[1,2,0,67108863,149],"matchCount": 0}, + {"elements":["#world"],"filters":["[NAME]=United States","[BLUE]=red"],"specificity":[1,0,2,67108863,90],"matchCount": 0}, + {"elements":["#world"],"filters":["[NAME]=United States"],"specificity":[1,0,1,67108863,66],"matchCount": 0}, + {"elements":["#countries"],"specificity":[1,0,0,67108863,241],"matchCount": 0}, + {"elements":["#countries"],"specificity":[1,0,0,67108863,194],"matchCount": 0}, + {"elements":["#world"],"specificity":[1,0,0,67108863,194],"matchCount": 0}, + {"elements":["#world"],"specificity":[1,0,0,67108863,11],"matchCount": 0} ] diff --git a/test/specificity/filters_and_ids.result b/test/specificity/filters_and_ids.result index abe3dcebf..d8568c960 100644 --- a/test/specificity/filters_and_ids.result +++ b/test/specificity/filters_and_ids.result @@ -1,8 +1,8 @@ [ - {"elements":["#world","#countries"],"filters":["[NAME]=United States"],"specificity":[2,0,1,94],"matchCount": 0}, - {"elements":["#world"],"filters":["[NAME]=United States"],"zoom":"......XXXXXXXXXXXXXXXXXXXX","specificity":[1,0,2,60],"matchCount": 0}, - {"elements":["#world"],"filters":["[NAME]=United States"],"specificity":[1,0,1,33],"matchCount": 0}, - {"elements":["#world"],"filters":["[NAME]=Canada"],"specificity":[1,0,1,7],"matchCount": 0}, - {"elements":[],"zoom":"......XXXXXXXXXXXXXXXXXXXX","specificity":[0,0,1,146],"matchCount": 0}, - {"elements":[],"filters":["[NAME]=United States"],"specificity":[0,0,1,120],"matchCount": 0} + {"elements":["#world","#countries"],"filters":["[NAME]=United States"],"specificity":[2,0,1,67108863,94],"matchCount": 0}, + {"elements":["#world"],"filters":["[NAME]=United States"],"zoom":"......XXXXXXXXXXXXXXXXXXXX","specificity":[1,0,2,67108800,60],"matchCount": 0}, + {"elements":["#world"],"filters":["[NAME]=United States"],"specificity":[1,0,1,67108863,33],"matchCount": 0}, + {"elements":["#world"],"filters":["[NAME]=Canada"],"specificity":[1,0,1,67108863,7],"matchCount": 0}, + {"elements":[],"zoom":"......XXXXXXXXXXXXXXXXXXXX","specificity":[0,0,1,67108800,146],"matchCount": 0}, + {"elements":[],"filters":["[NAME]=United States"],"specificity":[0,0,1,67108863,120],"matchCount": 0} ] diff --git a/test/specificity/issue60.result b/test/specificity/issue60.result index f24f92a94..01b8a46ba 100644 --- a/test/specificity/issue60.result +++ b/test/specificity/issue60.result @@ -1,6 +1,6 @@ [ - {"elements":["#world"],"filters":["[OBJECTID]=12"],"specificity":[1,0,1,131],"matchCount": 0}, - {"elements":["#world"],"filters":["[NET_INFLOW]>-10000"],"specificity":[1,0,1,83],"matchCount": 0}, - {"elements":["#world"],"filters":["[NET_INFLOW]>-30000"],"specificity":[1,0,1,35],"matchCount": 0}, - {"elements":["#world"],"specificity":[1,0,0,0],"matchCount": 0} + {"elements":["#world"],"filters":["[OBJECTID]=12"],"specificity":[1,0,1,67108863,131],"matchCount": 0}, + {"elements":["#world"],"filters":["[NET_INFLOW]>-10000"],"specificity":[1,0,1,67108863,83],"matchCount": 0}, + {"elements":["#world"],"filters":["[NET_INFLOW]>-30000"],"specificity":[1,0,1,67108863,35],"matchCount": 0}, + {"elements":["#world"],"specificity":[1,0,0,67108863,0],"matchCount": 0} ]