cartodb-4.42/lib/assets/javascripts/cartodb/models/carto.js

1047 lines
31 KiB
JavaScript
Raw Normal View History

2024-04-06 13:25:13 +08:00
carto_quotables = [
'text-face-name'
];
carto_variables = [
'text-name'
];
var carto_functionMap= {
'Equal Interval': 'equalInterval',
'Jenks': 'jenkBins',
'Heads/Tails': 'headTails',
'Quantile': 'quantileBins'
};
DEFAULT_QFUNCTION = 'Quantile';
/**
* Manage some carto properties depending on
* type (line, polygon or point), for choropleth.
*/
function manage_choropleth_props(type, props) {
var carto_props = {
'marker-width': props['marker-width'],
'marker-fill-opacity': props['marker-opacity'],
'marker-line-width': props['marker-line-width'],
'marker-line-color': props['marker-line-color'],
'marker-line-opacity': props['marker-line-opacity'],
'marker-allow-overlap': props['marker-allow-overlap'],
'line-color': props['line-color'],
'line-opacity': props['line-opacity'],
'line-width': props['line-width'],
'polygon-opacity': type == "line" ? 0 : props['polygon-opacity'],
'text-name': props['text-name'],
'text-halo-fill': props['text-halo-fill'],
'text-halo-radius': props['text-halo-radius'],
'text-face-name': props['text-face-name'],
'text-size': props['text-size'],
'text-dy': props['text-dy'],
'text-allow-overlap': props['text-allow-overlap'],
'text-placement': props['text-placement'],
'text-placement-type': props['text-placement-type'],
'text-label-position-tolerance': props['text-label-position-tolerance'],
'text-fill': props['text-fill']
}
// Remove all undefined properties
_.each(carto_props, function(v, k){
if(v === undefined) delete carto_props[k];
});
return carto_props;
}
function getProp(obj, prop) {
var p = [];
for(var k in obj) {
var v = obj[k];
if (k === prop) {
p.push(v);
} else if (typeof(v) === 'object') {
p = p.concat(getProp(v, prop));
}
}
return p;
}
var _cartocss_spec_props = getProp(carto.default_reference.version.latest, 'css');
/**
* some carto properties depends on others, this function
* remove or add properties needed to carto works
*/
function manage_carto_properies(props) {
if(/none/i.test(props['text-name']) || !props['text-name']) {
// remove all text-* properties
for(var p in props) {
if(isTextProperty(p)) {
delete props[p];
}
}
}
if(/none/i.test(props['polygon-comp-op'])) {
delete props['polygon-comp-op'];
}
if(/none/i.test(props['line-comp-op'])) {
delete props['line-comp-op'];
}
if(/none/i.test(props['marker-comp-op'])) {
delete props['marker-comp-op'];
}
// if polygon-pattern-file is present polygon-fill should be removed
if('polygon-pattern-file' in props) {
delete props['polygon-fill'];
}
delete props.zoom;
// translate props
props = translate_carto_properties(props);
return _.pick(props, _cartocss_spec_props);
}
function isTextProperty(p) {
return /^text-/.test(p);
}
function generate_carto_properties(props) {
return _(props).map(function(v, k) {
if(_.include(carto_quotables, k)) {
v = "'" + v + "'";
}
if(_.include(carto_variables, k)) {
v = "[" + v + "]";
}
return " " + k + ": " + v + ";";
});
}
function filter_props(props, fn) {
var p = {};
for(var k in props) {
var v = props[k];
if(fn(k, v)) {
p[k] = v;
}
}
return p;
}
function translate_carto_properties(props) {
if ('marker-opacity' in props) {
props['marker-fill-opacity'] = props['marker-opacity'];
delete props['marker-opacity'];
}
return props;
}
function simple_polygon_generator(table, props, changed, callback) {
// remove unnecesary properties, for example
// if the text-name is not present remove all the
// properties related to text
props = manage_carto_properies(props);
var text_properties = filter_props(props, function(k, v) { return isTextProperty(k); });
var general_properties = filter_props(props, function(k, v) { return !isTextProperty(k); });
// generate cartocss with the properties
generalLayerProps = generate_carto_properties(general_properties);
textLayerProps = generate_carto_properties(text_properties);
// layer with non-text properties
var generalLayer = "#" + table.getUnqualifiedName() + "{\n" + generalLayerProps.join('\n') + "\n}";
var textLayer = '';
if (_.size(textLayerProps)) {
textLayer = "\n\n#" + table.getUnqualifiedName() + "::labels {\n" + textLayerProps.join('\n') + "\n}\n";
}
// text properties layer
callback(generalLayer + textLayer);
}
function intensity_generator(table, props, changed, callback) {
// remove unnecesary properties, for example
// if the text-name is not present remove all the
// properties related to text
props = manage_carto_properies(props);
var carto_props = {
'marker-fill': props['marker-fill'],
'marker-width': props['marker-width'],
'marker-line-color': props['marker-line-color'],
'marker-line-width': props['marker-line-width'],
'marker-line-opacity': props['marker-line-opacity'],
'marker-fill-opacity': props['marker-fill-opacity'],
'marker-comp-op': 'multiply',
'marker-type': 'ellipse',
'marker-placement': 'point',
'marker-allow-overlap': true,
'marker-clip': false,
'marker-multi-policy': 'largest'
};
var table_name = table.getUnqualifiedName();
var css = "\n#" + table_name +"{\n";
_(carto_props).each(function(prop, name) {
css += " " + name + ": " + prop + "; \n";
});
css += "}";
callback(css);
}
function cluster_sql(table, zoom, props, nquartiles) {
var grids = ["A", "B", "C", "D", "E"];
var bucket = "bucket" + grids[0];
var mainBucket = bucket;
var sizes = [];
var step = 1 / (nquartiles + 1);
for (var i = 0; i < nquartiles; i++) {
sizes.push( 1 - step * i)
}
var sql = "WITH meta AS ( " +
" SELECT greatest(!pixel_width!,!pixel_height!) as psz, ext, ST_XMin(ext) xmin, ST_YMin(ext) ymin FROM (SELECT !bbox! as ext) a " +
" ), " +
" filtered_table AS ( " +
" SELECT t.* FROM <%= table %> t, meta m WHERE t.the_geom_webmercator && m.ext " +
" ), ";
for (var i = 0; i<nquartiles; i++) {
bucket = "bucket" + grids[i];
if (i == 0){
sql += mainBucket + "_snap AS (SELECT ST_SnapToGrid(f.the_geom_webmercator, 0, 0, m.psz * <%= size %>, m.psz * <%= size %>) the_geom_webmercator, count(*) as points_count, 1 as cartodb_id, array_agg(f.cartodb_id) AS id_list "
}
if (i > 0){
sql += "\n" + bucket + "_snap AS (SELECT ST_SnapToGrid(f.the_geom_webmercator, 0, 0, m.psz * " + sizes[i] + " * <%= size %>, m.psz * " + sizes[i] + " * <%= size %>) the_geom_webmercator, count(*) as points_count, 1 as cartodb_id, array_agg(f.cartodb_id) AS id_list "
}
sql += " FROM filtered_table f, meta m "
if (i == 0){
sql += " GROUP BY ST_SnapToGrid(f.the_geom_webmercator, 0, 0, m.psz * <%= size %>, m.psz * <%= size %>), m.xmin, m.ymin), ";
}
if (i > 0){
sql += " WHERE cartodb_id NOT IN (select unnest(id_list) FROM " + mainBucket + ") ";
for (var j = 1; j<i; j++) {
bucket2 = "bucket" + grids[j];
sql += " AND cartodb_id NOT IN (select unnest(id_list) FROM " + bucket2 + ") ";
}
sql += " GROUP BY ST_SnapToGrid(f.the_geom_webmercator, 0, 0, m.psz * " + sizes[i] + " * <%= size %>, m.psz * " + sizes[i] + " * <%= size %>), m.xmin, m.ymin), ";
}
sql += bucket + " AS (SELECT * FROM " + bucket + "_snap WHERE points_count > ";
if (i == nquartiles - 1) {
sql += " GREATEST(<%= size %> * 0.1, 2) ";
} else {
sql += " <%= size %> * " + sizes[i];
}
sql += " ) ";
if (i < nquartiles - 1) sql += ", ";
}
sql += " SELECT the_geom_webmercator, 1 points_count, cartodb_id, ARRAY[cartodb_id] as id_list, 'origin' as src, cartodb_id::text cdb_list FROM filtered_table WHERE ";
for (var i = 0; i < nquartiles; i++) {
bucket = "bucket" + grids[i];
sql += "\n" + (i > 0 ? "AND " : "") + "cartodb_id NOT IN (select unnest(id_list) FROM " + bucket + ") ";
}
for (var i = 0; i < nquartiles; i++) {
bucket = "bucket" + grids[i];
sql += " UNION ALL SELECT *, '" + bucket + "' as src, array_to_string(id_list, ',') cdb_list FROM " + bucket
}
return _.template(sql, {
name: table.get("name"),
//size: props["radius_min"],
size: 48,
table: "__wrapped"
});
}
function cluster_generator(table, props, changed, callback) {
var methodMap = {
'2 Buckets': 2,
'3 Buckets': 3,
'4 Buckets': 4,
'5 Buckets': 5,
};
var grids = ["A", "B", "C", "D", "E"];
var nquartiles = methodMap[props['method']];
var table_name = table.getUnqualifiedName();
var sql = cluster_sql(table, props.zoom, props, nquartiles);
var c = "#" + table_name + "{\n";
c += " marker-width: " + (Math.round(props["radius_min"]/2)) + ";\n";
c += " marker-fill: " + props['marker-fill'] + ";\n";
c += " marker-line-width: 1.5;\n";
c += " marker-fill-opacity: " + props['marker-opacity'] + ";\n";
c += " marker-line-opacity: " + props['marker-line-opacity'] + ";\n";
c += " marker-line-color: " + props['marker-line-color'] + ";\n";
c += " marker-allow-overlap: true;\n";
var base = 20;
var min = props["radius_min"];
var max = props["radius_max"];
var sizes = [min];
var step = Math.round((max-min)/ (nquartiles - 1));
for (var i = 1; i < nquartiles - 1; i++) {
sizes.push(min + step * i);
}
sizes.push(max);
for (var i = 0; i < nquartiles; i++) {
c += "\n [src = 'bucket"+grids[nquartiles - i - 1]+"'] {\n";
c += " marker-line-width: " + props['marker-line-width'] + ";\n";
c += " marker-width: " + sizes[i] + ";\n";
c += " } \n";
}
c += "}\n\n";
// Generate label properties
c += "#" + table.getUnqualifiedName() + "::labels { \n";
c += " text-size: 0; \n";
c += " text-fill: " + props['text-fill'] + "; \n";
c += " text-opacity: 0.8;\n";
c += " text-name: [points_count]; \n";
c += " text-face-name: '" + props['text-face-name'] + "'; \n";
c += " text-halo-fill: " + props['text-halo-fill'] + "; \n";
c += " text-halo-radius: 0; \n";
for (var i = 0; i < nquartiles; i++) {
c += "\n [src = 'bucket"+grids[nquartiles - i - 1]+"'] {\n";
c += " text-size: " + (i * 5 + 12) + ";\n";
c += " text-halo-radius: " + props['text-halo-radius'] + ";";
c += "\n }\n";
}
c += "\n text-allow-overlap: true;\n\n";
c += " [zoom>11]{ text-size: " + Math.round(props["radius_min"] * 0.66) + "; }\n";
c += " [points_count = 1]{ text-size: 0; }\n";
c += "}\n";
callback(c, {}, sql);
}
function bubble_generator(table, props, changed, callback) {
var carto_props = {
'marker-fill': props['marker-fill'],
'marker-line-color': props['marker-line-color'],
'marker-line-width': props['marker-line-width'],
'marker-line-opacity': props['marker-line-opacity'],
'marker-fill-opacity': props['marker-opacity'],
'marker-comp-op': props['marker-comp-op'],
'marker-placement': 'point',
'marker-type': 'ellipse',
'marker-allow-overlap': true,
'marker-clip':false,
'marker-multi-policy':'largest'
};
var prop = props['property'];
var min = props['radius_min'];
var max = props['radius_max'];
var fn = carto_functionMap[props['qfunction'] || DEFAULT_QFUNCTION];
if(/none/i.test(props['marker-comp-op'])) {
delete carto_props['marker-comp-op'];
}
var values = [];
var NPOINS = 10;
// TODO: make this related to the quartiles size
// instead of linear. The circle area should be related
// to the data and a little correction due to the problems
// humans have to measure the area of a circle
//calculate the bubles sizes
for(var i = 0; i < NPOINS; ++i) {
var t = i/(NPOINS-1);
values.push(min + t*(max - min));
}
// generate carto
simple_polygon_generator(table, carto_props, changed, function(css) {
var table_name = table.getUnqualifiedName();
table.data()[fn](NPOINS, prop, function(quartiles) {
for(var i = NPOINS - 1; i >= 0; --i) {
if(quartiles[i] !== undefined && quartiles[i] != null) {
css += "\n#" + table_name +" [ " + prop + " <= " + quartiles[i] + "] {\n"
css += " marker-width: " + values[i].toFixed(1) + ";\n}"
}
}
callback(css, quartiles);
});
});
}
/**
* when quartiles are greater than 1<<31 cast to float added .01
* at the end. If you append only .0 it is casted to int and it
* does not work
*/
function normalizeQuartiles(quartiles) {
var maxNumber = 2147483648; // unsigned (1<<31);
var normalized = [];
for(var i = 0; i < quartiles.length; ++i) {
var q = quartiles[i];
if(q > Math.abs(maxNumber) && String(q).indexOf('.') === -1) {
q = q + ".01";
}
normalized.push(q);
}
return normalized;
}
function choropleth_generator(table, props, changed, callback) {
var type = table.geomColumnTypes() && table.geomColumnTypes()[0] || "polygon";
var carto_props = manage_choropleth_props(type,props);
if(props['polygon-comp-op'] && !/none/i.test(props['polygon-comp-op'])) {
carto_props['polygon-comp-op'] = props['polygon-comp-op'];
}
if(props['line-comp-op'] && !/none/i.test(props['line-comp-op'])) {
carto_props['line-comp-op'] = props['line-comp-op'];
}
if(props['marker-comp-op'] && !/none/i.test(props['marker-comp-op'])) {
carto_props['marker-comp-op'] = props['marker-comp-op'];
}
var methodMap = {
'3 Buckets': 3,
'5 Buckets': 5,
'7 Buckets': 7
};
if(!props['color_ramp']) {
return;
}
var fn = carto_functionMap[props['qfunction'] || DEFAULT_QFUNCTION];
var prop = props['property'];
var nquartiles = methodMap[props['method']];
var ramp = cdb.admin.color_ramps[props['color_ramp']][nquartiles];
if(!ramp) {
cdb.log.error("no color ramp defined for " + nquartiles + " quartiles");
} else {
if (type == "line") {
carto_props["line-color"] = ramp[0];
} else if (type == "polygon") {
carto_props["polygon-fill"] = ramp[0];
} else {
carto_props["marker-fill"] = ramp[0];
}
}
simple_polygon_generator(table, carto_props, changed, function(css) {
var table_name = table.getUnqualifiedName();
table.data()[fn](nquartiles, prop, function(quartiles) {
quartiles = normalizeQuartiles(quartiles);
for(var i = nquartiles - 1; i >= 0; --i) {
if(quartiles[i] !== undefined && quartiles[i] != null) {
css += "\n#" + table_name +" [ " + prop + " <= " + quartiles[i] + "] {\n";
if (type == "line") {
css += " line-color: " + ramp[i] + ";\n}"
} else if (type == "polygon") {
css += " polygon-fill: " + ramp[i] + ";\n}"
} else {
css += " marker-fill: " + ramp[i] + ";\n}"
}
}
}
callback(css, quartiles);
});
});
}
function density_sql(table, zoom, props) {
var prop = 'cartodb_id';
var sql;
// we generate a grid and get the number of points
// for each cell. With that the density is generated
// and calculated for zoom level 10, which is taken as reference when we calculate the quartiles for the style buclets
// see models/carto.js
if(props['geometry_type'] === 'Rectangles') {
sql = "WITH hgrid AS (SELECT CDB_RectangleGrid(ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * <%= size %>), greatest(!pixel_width!,!pixel_height!) * <%= size %>, greatest(!pixel_width!,!pixel_height!) * <%= size %>) as cell) SELECT hgrid.cell as the_geom_webmercator, count(i.<%=prop%>) as points_count,count(i.<%=prop%>)/power( <%= size %> * CDB_XYZ_Resolution(<%= z %>), 2 ) as points_density, 1 as cartodb_id FROM hgrid, <%= table %> i where ST_Intersects(i.the_geom_webmercator, hgrid.cell) GROUP BY hgrid.cell";
} else {
sql = "WITH hgrid AS (SELECT CDB_HexagonGrid(ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * <%= size %>), greatest(!pixel_width!,!pixel_height!) * <%= size %>) as cell) SELECT hgrid.cell as the_geom_webmercator, count(i.<%=prop%>) as points_count, count(i.<%=prop%>)/power( <%= size %> * CDB_XYZ_Resolution(<%= z %>), 2 ) as points_density, 1 as cartodb_id FROM hgrid, <%= table %> i where ST_Intersects(i.the_geom_webmercator, hgrid.cell) GROUP BY hgrid.cell";
}
return _.template(sql, {
prop: prop,
table: '__wrapped',
size: props['polygon-size'],
z: zoom
});
}
/*
*
*/
function density_generator(table, props, changed, callback) {
var carto_props = {
'line-color': props['line-color'],
'line-opacity': props['line-opacity'],
'line-width': props['line-width'],
'polygon-opacity': props['polygon-opacity'],
'polygon-comp-op': props['polygon-comp-op']
}
if(/none/i.test(props['polygon-comp-op'])) {
delete carto_props['polygon-comp-op'];
}
var methodMap = {
'3 Buckets': 3,
'5 Buckets': 5,
'7 Buckets': 7
};
var polygon_size = props['polygon-size'];
var nquartiles = methodMap[props['method']];
var ramp = cdb.admin.color_ramps[props['color_ramp']][nquartiles];
if(!ramp) {
cdb.log.error("no color ramp defined for " + nquartiles + " quartiles");
}
carto_props['polygon-fill'] = ramp[ramp.length - 1];
var density_sql_gen = density_sql(table, props.zoom, props);
simple_polygon_generator(table, carto_props, changed, function(css) {
// density
var tmpl = _.template("" +
"WITH clusters as ( " +
"SELECT " +
"cartodb_id, " +
"st_snaptogrid(the_geom_webmercator, <%= polygon_size %>*CDB_XYZ_Resolution(<%= z %>)) as center " +
"FROM <%= table_name %>" +
"), " +
"points as ( " +
"SELECT " +
"count(cartodb_id) as npoints, " +
"count(cartodb_id)/power( <%= polygon_size %> * CDB_XYZ_Resolution(<%= z %>), 2 ) as density " +
"FROM " +
"clusters " +
"group by " +
"center " +
"), " +
"stats as ( " +
"SELECT " +
"npoints, " +
"density, " +
"ntile(<%= slots %>) over (order by density) as quartile " +
"FROM points " +
") " +
"SELECT " +
"quartile, " +
"max(npoints) as maxAmount, " +
"max(density) as maxDensity " +
"FROM stats " +
"GROUP BY quartile ORDER BY quartile ");
var sql = tmpl({
slots: nquartiles,
table_name: table.get('name'),
polygon_size: polygon_size,
z: props.zoom
});
table.data()._sqlQuery(sql, function(data) {
// extract quartiles by zoom level
var rows = data.rows;
var quartiles = [];
for(var i = 0; i < rows.length; ++i) {
quartiles.push(rows[i].maxdensity);
}
quartiles = normalizeQuartiles(quartiles);
var table_name = table.getUnqualifiedName();
css += "\n#" + table_name + "{\n"
for(var i = nquartiles - 1; i >= 0; --i) {
if(quartiles[i] !== undefined) {
css += " [points_density <= " + quartiles[i] + "] { polygon-fill: " + ramp[i] + "; }\n";
}
}
css += "\n}"
callback(css, quartiles, density_sql_gen);
});
});
}
cdb.admin.CartoStyles = Backbone.Model.extend({
defaults: {
type: 'polygon',
properties: {
'polygon-fill': '#FF6600',
'line-color': '#FFFFFF',
'line-width': 1,
'polygon-opacity': 0.7,
'line-opacity':1
}
},
initialize: function() {
this.table = this.get('table');
if (!this.table) {
throw "table must be passed as param"
return;
}
this.properties = new cdb.core.Model(this.get('properties'));
this.bind('change:properties', this._generateCarto, this);
this.generators = {};
this.registerGenerator('polygon', simple_polygon_generator);
this.registerGenerator('cluster', cluster_generator);
this.registerGenerator('bubble', bubble_generator);
this.registerGenerator('intensity', intensity_generator);
this.registerGenerator('choropleth', choropleth_generator);
this.registerGenerator('color', cdb.admin.carto.category.category_generator.bind(cdb.admin.carto.category)),
this.registerGenerator('category', cdb.admin.carto.category.category_generator.bind(cdb.admin.carto.category)),
this.registerGenerator('density', density_generator); // the same generator than choroplet
this.registerGenerator('torque', cdb.admin.carto.torque.torque_generator.bind(cdb.admin.carto.torque));
this.registerGenerator('torque_heat', cdb.admin.carto.torque.torque_generator.bind(cdb.admin.carto.torque));
this.registerGenerator('torque_cat', cdb.admin.carto.torque_cat.generate.bind(cdb.admin.carto.torque_cat));
},
// change a property attribute
attr: function(name, val) {
var old = this.attributes.properties[name];
this.attributes.properties[name] = val;
if(old != val) {
this.trigger('change:properties', this, this.attributes.properties);
this.trigger('changes', this);
}
},
registerGenerator: function(name, gen) {
this.generators[name] = gen;
},
/**
* generate a informative header
*/
_generateHeader: function() {
var typeMap = {
'polygon': 'simple'
}
var t = this.get('type');
t = typeMap[t] || t;
var c = "/** " + t + " visualization */\n\n";
return c;
},
regenerate: function() {
//TODO: apply patch if it's possible
this._generateCarto();
},
_generateCarto: function(){
var self = this;
var gen = this.generators[this.get('type')];
var gen_type = this.get('type');
if(!gen) {
cdb.log.info("can't get style generator for " + this.get('type'));
return;
}
// Get changed properties
var changed = {};
this.properties.bind('change', function() {
changed = this.properties.changedAttributes();
}, this);
this.properties.set(this.get('properties'));
this.properties.unbind('change', null, this);
this.trigger('loading');
gen(this.table, this.get('properties'), changed, function(style, metadata, sql) {
if (self.get('type') !== gen_type) {
return;
}
var attrs = {
style: self._generateHeader() + style
};
if(sql) {
attrs.sql = sql;
} else {
self.unset('sql', { silent: true });
}
if (metadata) {
attrs.metadata = metadata;
}
self.set(attrs, { silent: true });
self.change({ changes: {'style': ''}});
self.trigger('load');
})
}
}, {
DEFAULT_GEOMETRY_STYLE: "{\n // points\n [mapnik-geometry-type=point] {\n marker-fill: #FF6600;\n marker-opacity: 1;\n marker-width: 12;\n marker-line-color: white;\n marker-line-width: 3;\n marker-line-opacity: 0.9;\n marker-placement: point;\n marker-type: ellipse;marker-allow-overlap: true;\n }\n\n //lines\n [mapnik-geometry-type=linestring] {\n line-color: #FF6600; \n line-width: 2; \n line-opacity: 0.7;\n }\n\n //polygons\n [mapnik-geometry-type=polygon] {\n polygon-fill:#FF6600;\n polygon-opacity: 0.7;\n line-opacity:1;\n line-color: #FFFFFF;\n }\n }",
});
/**
* this class provides methods to parse and extract information from the
* cartocss like expressions used, filters, colors and errors
*/
cdb.admin.CartoParser = function(cartocss) {
this.parse_env = null;
this.ruleset = null;
if(cartocss) {
this.parse(cartocss);
}
}
cdb.admin.CartoParser.prototype = {
RESERVED_VARIABLES: ['mapnik-geometry-type', 'points_density', 'points_count', 'src', 'value'], // value due to torque
parse: function(cartocss) {
var self = this;
var parse_env = this.parse_env = {
validation_data: false,
frames: [],
errors: [],
error: function(obj) {
obj.line = carto.Parser().extractErrorLine(cartocss, obj.index);
this.errors.push(obj);
}
};
var ruleset = null;
var defs = null;
try {
// set default reference
carto.tree.Reference.setData(carto.default_reference.version.latest);
ruleset = (new carto.Parser(parse_env)).parse(cartocss);
} catch(e) {
// add the style.mss string to match the response from the server
this.parse_env.errors = this._parseError(["style\.mss" + e.message])
return;
}
if(ruleset) {
var existing = {}
this.definitions = defs = ruleset.toList(parse_env);
var mapDef;
for(var i in defs){
if(defs[i].elements.length > 0){
if(defs[i].elements[0].value === "Map"){
mapDef = defs.splice(i, 1)[0];
}
}
}
var symbolizers = torque.cartocss_reference.version.latest.layer;
if (mapDef){
mapDef.rules.forEach(function(r){
var key = r.name;
if (!(key in symbolizers)) {
parse_env.error({
message: 'Rule ' + key + ' not allowed for Map.',
index: r.index
});
}
else{
var type = symbolizers[r.name].type;
var element = r.value.value[0].value[0];
if(!self._checkValidType(element, type)){
parse_env.error({
message: 'Expected type ' + type + '.' ,
index: r.index
});
}
}
});
}
var defs = carto.inheritDefinitions(defs, parse_env);
defs = carto.sortStyles(defs, parse_env);
for (var i in defs) {
for (var j in defs[i]) {
var r = defs[i][j]
if(r && r.toXML) {
r.toXML(parse_env, existing);
}
}
}
// toList uses parse_env.errors.message to put messages
if (parse_env.errors.message) {
_(parse_env.errors.message.split('\n')).each(function(m) {
parse_env.errors.push(m);
});
}
}
this.ruleset = ruleset;
return this;
},
_checkValidType: function(e, type){
if (["number", "float"].indexOf(type) > -1) {
return typeof e.value === "number";
}
else if (type === "string"){
return e.value !== "undefined" && typeof e.value === "string";
}
else if (type.constructor === Array){
return type.indexOf(e.value) > -1 || e.value === "linear";
}
else if (type === "color"){
return checkValidColor(e);
}
return true;
},
_checkValidColor: function(e){
var expectedArguments = { rgb: 3, hsl: 3, rgba: 4, hsla: 4};
return typeof e.rgb !== "undefined" || expectedArguments[e.name] === e.args;
},
/**
* gets an array of parse errors from windshaft
* and returns an array of {line:1, error: 'string'] with user friendly
* strings. Parses errors in format:
*
* 'style.mss:7:2 Invalid code: asdasdasda'
*/
_parseError: function(errors) {
var parsedErrors = [];
for(var i in errors) {
var err = errors[i];
if(err && err.length > 0) {
var g = err.match(/.*:(\d+):(\d+)\s*(.*)/);
if(g) {
parsedErrors.push({
line: parseInt(g[1], 10),
message: g[3]
});
} else {
parsedErrors.push({
line: null,
message: err
})
}
}
}
// sort by line
parsedErrors.sort(function(a, b) { return a.line - b.line; });
parsedErrors = _.uniq(parsedErrors, true, function(a) { return a.line + a.message; });
return parsedErrors;
},
/**
* return the error list, empty if there were no errors
*/
errors: function() {
return this.parse_env ? this.parse_env.errors : [];
},
_colorsFromRule: function(rule) {
var self = this;
function searchRecursiveByType(v, t) {
var res = []
for(var i in v) {
if(v[i] instanceof t) {
res.push(v[i]);
} else if(typeof(v[i]) === 'object') {
var r = searchRecursiveByType(v[i], t);
if(r.length) {
res = res.concat(r);
}
}
}
return res;
}
return searchRecursiveByType(rule.ev(this.parse_env), carto.tree.Color);
},
_varsFromRule: function(rule) {
function searchRecursiveByType(v, t) {
var res = []
for(var i in v) {
if(v[i] instanceof t) {
res.push(v[i]);
} else if(typeof(v[i]) === 'object') {
var r = searchRecursiveByType(v[i], t);
if(r.length) {
res = res.concat(r);
}
}
}
return res;
}
return searchRecursiveByType(rule, carto.tree.Field);
},
/**
* Extract information from the carto using the provided method.
* */
_extract: function(method, extractVariables) {
var columns = [];
if (this.ruleset) {
var definitions = this.ruleset.toList(this.parse_env);
for (var d in definitions) {
var def = definitions[d];
if(def.filters) {
// extract from rules
for(var r in def.rules) {
var rule = def.rules[r];
var columnList = method(this, rule);
columns = columns.concat(columnList);
}
if (extractVariables) {
for(var f in def.filters) {
var filter = def.filters[f];
for (var k in filter) {
var filter_key = filter[k]
if (filter_key.key && filter_key.key.value) {
columns.push(filter_key.key.value);
}
}
}
}
}
}
var self = this;
return _.reject(_.uniq(columns), function(v) {
return _.contains(self.RESERVED_VARIABLES, v);
});
}
},
/**
* return a list of colors used in cartocss
*/
colorsUsed: function(opt) {
// extraction method
var method = function(self, rule) {
return _.map(self._colorsFromRule(rule), function(f) {
return f.rgb;
})
};
var colors = this._extract(method, false);
if (opt && opt.mode == 'hex') {
colors = _.map(colors, function(color) {
return cdb.Utils.rgbToHex(color[0], color[1], color[2]);
});
}
return colors;
},
/**
* return a list of variables used in cartocss
*/
variablesUsed: function() {
// extraction method
var method = function(self, rule) {
return _.map(self._varsFromRule(rule), function(f) {
return f.value;
});
};
return this._extract(method, true);
},
/**
* returns the default layer
*/
getDefaultRules: function() {
var rules = [];
for(var i = 0; i < this.definitions.length; ++i) {
var def = this.definitions[i];
// all zooms and default attachment so we don't get conditional variables
if (def.zoom === 8388607 && _.size(def.filters.filters) === 0 && def.attachment === '__default__') {
rules = rules.concat(def.rules);
}
}
var rulesMap = {};
for (var r in rules) {
var rule = rules[r];
rulesMap[rule.name] = rule;
}
return rulesMap;
},
getRuleByName: function(definition, ruleName) {
if (!definition._rulesByName) {
var rulesMap = definition._rulesByName = {};
for (var r in definition.rules) {
var rule = definition.rules[r];
rulesMap[rule.name] = rule;
}
}
return definition._rulesByName[ruleName];
}
};