1047 lines
31 KiB
JavaScript
1047 lines
31 KiB
JavaScript
|
|
|
|
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];
|
|
}
|
|
|
|
|
|
};
|
|
|