Initial commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// Get the source node from an analysis node
|
||||
function getSourceNode (nodeModel) {
|
||||
var source;
|
||||
if (nodeModel.get('type') === 'source') {
|
||||
source = nodeModel;
|
||||
} else {
|
||||
var primarySource = nodeModel.getPrimarySource && nodeModel.getPrimarySource();
|
||||
if (primarySource && primarySource.get('type') === 'source') {
|
||||
source = primarySource;
|
||||
} else {
|
||||
source = getSourceNode(primarySource);
|
||||
}
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
// Evaluates predicate for the analysis chain from nodeModel
|
||||
function someNode (nodeModel, predicate) {
|
||||
if (predicate(nodeModel)) {
|
||||
return nodeModel;
|
||||
}
|
||||
|
||||
if (nodeModel.get('type') !== 'source') {
|
||||
const source = nodeModel.getPrimarySource && nodeModel.getPrimarySource();
|
||||
|
||||
if (source) {
|
||||
return someNode(source, predicate);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasAnalysisType (type) {
|
||||
return function (node) {
|
||||
return node.get('type') === type;
|
||||
};
|
||||
}
|
||||
|
||||
const hasTradeArea = hasAnalysisType('trade-area');
|
||||
const hasSQLFunction = hasAnalysisType('deprecated-sql-function');
|
||||
|
||||
module.exports = {
|
||||
nodeHasTradeArea: node => someNode(node, hasTradeArea),
|
||||
nodeHasSQLFunction: node => someNode(node, hasSQLFunction),
|
||||
|
||||
// These are defined outside because they're recursive
|
||||
getSourceNode,
|
||||
someNode
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Just provide info about the current browser
|
||||
*/
|
||||
|
||||
module.exports = function () {
|
||||
var ua = navigator.userAgent;
|
||||
var tem;
|
||||
var M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
||||
if (/trident/i.test(M[1])) {
|
||||
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
|
||||
return {
|
||||
name: 'IE ',
|
||||
version: (tem[1] || '')
|
||||
};
|
||||
}
|
||||
if (M[1] === 'Chrome') {
|
||||
tem = ua.match(/\bOPR\/(\d+)/);
|
||||
if (tem != null) {
|
||||
return {
|
||||
name: 'Opera',
|
||||
version: tem[1]
|
||||
};
|
||||
}
|
||||
}
|
||||
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
|
||||
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
|
||||
M.splice(1, 1, tem[1]);
|
||||
}
|
||||
|
||||
return {
|
||||
name: M[0],
|
||||
version: M[1]
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
var _ = require('underscore');
|
||||
var CartoColors = require('cartocolor');
|
||||
|
||||
var Tags = {
|
||||
COLORBLIND: 'colorblind',
|
||||
DIVERGING: 'diverging',
|
||||
QUALITATIVE: 'qualitative',
|
||||
QUANTITATIVE: 'quantitative'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getColorRamps: function (colorsNumber) {
|
||||
if (!colorsNumber) { throw new Error('Number of colors per ramp is required'); }
|
||||
|
||||
var rampColors = [];
|
||||
|
||||
_.each(CartoColors, function (color) {
|
||||
if (color && color[colorsNumber]) {
|
||||
rampColors.push(color[colorsNumber]);
|
||||
}
|
||||
});
|
||||
|
||||
return rampColors;
|
||||
},
|
||||
|
||||
getColorRampsByTag: function (colorsNumber, tag) {
|
||||
if (!colorsNumber) { throw new Error('Number of colors per ramp is required'); }
|
||||
|
||||
var rampColors = [];
|
||||
|
||||
_.each(CartoColors, function (color) {
|
||||
if (color && color[colorsNumber] && _.contains(color.tags, tag)) {
|
||||
rampColors.push(color[colorsNumber]);
|
||||
}
|
||||
});
|
||||
|
||||
return rampColors;
|
||||
},
|
||||
|
||||
getColorblindRamps: function (colorsNumber) {
|
||||
return this.getColorRampsByTag(colorsNumber, Tags.COLORBLIND);
|
||||
},
|
||||
|
||||
getDivergingRamps: function (colorsNumber) {
|
||||
return this.getColorRampsByTag(colorsNumber, Tags.DIVERGING);
|
||||
},
|
||||
|
||||
getQualitativeRamps: function (colorsNumber) {
|
||||
return this.getColorRampsByTag(colorsNumber, Tags.QUALITATIVE);
|
||||
},
|
||||
|
||||
getQuantitativeRamps: function (colorsNumber) {
|
||||
return this.getColorRampsByTag(colorsNumber, Tags.QUANTITATIVE);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
module.exports = [
|
||||
'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige',
|
||||
'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown',
|
||||
'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue',
|
||||
'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod',
|
||||
'darkgray', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen',
|
||||
'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen',
|
||||
'darkslateblue', 'darkslategray', 'darkturquoise', 'darkviolet',
|
||||
'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue', 'firebrick',
|
||||
'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite',
|
||||
'gold', 'goldenrod', 'gray', 'grey', 'green', 'greenyellow', 'honeydew',
|
||||
'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender',
|
||||
'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral',
|
||||
'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightpink',
|
||||
'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray',
|
||||
'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta',
|
||||
'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple',
|
||||
'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
|
||||
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
|
||||
'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered',
|
||||
'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred',
|
||||
'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue',
|
||||
'purple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
|
||||
'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue',
|
||||
'slateblue', 'slategray', 'snow', 'springgreen', 'steelblue', 'tan',
|
||||
'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white',
|
||||
'whitesmoke', 'yellow', 'yellowgreen'
|
||||
];
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
var _ = require('underscore');
|
||||
var DEFAULT_ERROR_MSG = '';
|
||||
|
||||
/**
|
||||
* Return error message when backend request fails
|
||||
* It tries to get responseText > errors and error arrays, if not gets `statusText`.
|
||||
*/
|
||||
|
||||
module.exports = function (e) {
|
||||
if (!e) { throw new Error('error is required'); }
|
||||
|
||||
try {
|
||||
var responseText = e.responseText && e.responseText.trim() && JSON.parse(e.responseText);
|
||||
var errorMessage = e.statusText || DEFAULT_ERROR_MSG;
|
||||
|
||||
if (responseText) {
|
||||
var errors = _.compact(
|
||||
_.map(['errors', 'error'], function (type) {
|
||||
return responseText[type] && responseText[type].join(', ');
|
||||
})
|
||||
);
|
||||
errorMessage = errors.join(', ');
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
} catch (err) {
|
||||
return DEFAULT_ERROR_MSG;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
var track = function (error) {
|
||||
if (window.trackJs) {
|
||||
window.trackJs.track(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = track;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Fetch all query objects (querySchemaModel, queryGeometryModel, queryRowsCollection)
|
||||
* if necessary
|
||||
*/
|
||||
|
||||
module.exports = function (params) {
|
||||
if (!params) throw new Error('all query objects are required');
|
||||
if (!params.querySchemaModel) throw new Error('querySchemaModel is required');
|
||||
if (!params.queryGeometryModel) throw new Error('queryGeometryModel is required');
|
||||
if (!params.queryRowsCollection) throw new Error('queryRowsCollection is required');
|
||||
|
||||
function fetchModel (model) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var subscribeToFinalStatus = false;
|
||||
|
||||
if (model.shouldFetch()) {
|
||||
model.fetch();
|
||||
subscribeToFinalStatus = true;
|
||||
} else if (!model.isInFinalStatus()) {
|
||||
subscribeToFinalStatus = true;
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
if (subscribeToFinalStatus) {
|
||||
model.once('inFinalStatus', function () {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var schemaModelPromise = fetchModel(params.querySchemaModel);
|
||||
var geometryModelPromise = fetchModel(params.queryGeometryModel);
|
||||
|
||||
return Promise.all([schemaModelPromise, geometryModelPromise])
|
||||
.then(function () {
|
||||
return fetchModel(params.queryRowsCollection); // rows collection depends on schema
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Send events to Google Analytics if it is available
|
||||
* - https://developers.google.com/analytics/devguides/collection/analyticsjs/sending-hits
|
||||
*
|
||||
*/
|
||||
|
||||
var GAPusher = function (opts) {
|
||||
var ga = window.ga;
|
||||
opts = opts || {};
|
||||
|
||||
if (ga) {
|
||||
ga(opts.eventName || 'send', {
|
||||
hitType: opts.hitType,
|
||||
eventCategory: opts.eventCategory,
|
||||
eventAction: opts.eventAction,
|
||||
eventLabel: opts.eventLabel
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = GAPusher;
|
||||
@@ -0,0 +1,69 @@
|
||||
var _ = require('underscore');
|
||||
var ACTIVE_LOCALE = window.ACTIVE_LOCALE;
|
||||
var Locale = require('locale/index')[ACTIVE_LOCALE];
|
||||
|
||||
var LINK_TEMPLATE = _.template("<a href='<%- href %>'><%- link %></a>");
|
||||
|
||||
module.exports = {
|
||||
/*
|
||||
*
|
||||
* Sometimes we need to include a link in a message. This function takes a key:
|
||||
*
|
||||
* {
|
||||
* "body": "You are over platform's limits. Please %{link} to know more details",
|
||||
* "link": "contact us",
|
||||
* "href": "mailto:support@carto.com"
|
||||
* }
|
||||
*
|
||||
* and return the message htmlfied with the link properly included
|
||||
*/
|
||||
|
||||
linkify: function (key) {
|
||||
// microtemplate function
|
||||
// underscore doesn't allow change the template settings for one call
|
||||
function t (s, d) {
|
||||
for (var p in d) {
|
||||
s = s.replace(new RegExp('%{' + p + '}', 'g'), d[p]);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
var data = this.resolve(key);
|
||||
|
||||
if (!data) {
|
||||
return void 0;
|
||||
}
|
||||
|
||||
if (_.isString(data)) {
|
||||
// we need an object with body, href, link
|
||||
return data;
|
||||
}
|
||||
|
||||
var message = data.body;
|
||||
var link = data.link;
|
||||
var href = data.href;
|
||||
|
||||
// If some data is missing or it doesn't need link
|
||||
if (!message || !link || !href || !this.needLink(message)) {
|
||||
return void 0;
|
||||
}
|
||||
|
||||
return t(message, {
|
||||
link: LINK_TEMPLATE({
|
||||
link: link,
|
||||
href: href
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
needLink: function (message) {
|
||||
return message.indexOf('%{link}') >= 0;
|
||||
},
|
||||
|
||||
// It returns the value or undefined for a given locale key
|
||||
resolve: function (path) {
|
||||
return path.split('.').reduce(function (prev, curr) {
|
||||
return prev ? prev[curr] : void 0;
|
||||
}, Locale);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
var _ = require('underscore');
|
||||
var DEFAULT_VALUES = {
|
||||
top: 0,
|
||||
left: 0
|
||||
};
|
||||
var MIN_GAP = 10;
|
||||
var MIN_HEIGHT = 205;
|
||||
var MIN_WIDTH = 244;
|
||||
|
||||
/**
|
||||
* Would you like to know where to open a context menu?
|
||||
* Just provide:
|
||||
* @param {Object} options.parentView Parent element view
|
||||
* @param {Number} options.posX Position X of the context menu
|
||||
* @param {Number} options.posY Position Y of the context menu
|
||||
* @param {Number} options.elementWidth (optional) Context menu width
|
||||
* @param {Number} options.elementHeight (optional) Context menu height
|
||||
* @param {Number} options.offsetX (optional) Context menu offsetX
|
||||
* @param {Number} options.offsetY (optional) Context menu offsetY
|
||||
*
|
||||
* By default, it will try to positionate to the right (inside) and to
|
||||
* the top (below).
|
||||
*/
|
||||
|
||||
var MagicPositioner = function (params) {
|
||||
if (!params.parentView) throw new Error('parentView within params is required');
|
||||
if (!_.isNumber(params.posX)) throw new Error('posX within params is required');
|
||||
if (!_.isNumber(params.posY)) throw new Error('posY within params is required');
|
||||
|
||||
var $parentView = params.parentView;
|
||||
var parentViewHeight = $parentView.outerHeight();
|
||||
var parentViewWidth = $parentView.outerWidth();
|
||||
var posX = params.posX;
|
||||
var posY = params.posY;
|
||||
var offsetX = params.offsetX || 0;
|
||||
var offsetY = params.offsetY || 0;
|
||||
var elementWidth = params.elementWidth || MIN_WIDTH;
|
||||
var elementHeight = params.elementHeight || MIN_HEIGHT;
|
||||
var cssProps = DEFAULT_VALUES;
|
||||
|
||||
if ((posX - elementWidth) > MIN_GAP) {
|
||||
cssProps.left = 'auto';
|
||||
cssProps.right = (parentViewWidth - posX + offsetX) + 'px';
|
||||
} else if ((posX + elementWidth) > elementWidth) {
|
||||
cssProps.right = 'auto';
|
||||
cssProps.left = (posX + offsetX) + 'px';
|
||||
}
|
||||
|
||||
if ((posY + elementHeight) < parentViewHeight) {
|
||||
cssProps.bottom = 'auto';
|
||||
cssProps.top = (posY + offsetY) + 'px';
|
||||
} else if ((posY + elementHeight) > parentViewHeight) {
|
||||
cssProps.top = 'auto';
|
||||
cssProps.bottom = (parentViewHeight - posY + offsetY) + 'px';
|
||||
}
|
||||
|
||||
return cssProps;
|
||||
};
|
||||
|
||||
module.exports = MagicPositioner;
|
||||
@@ -0,0 +1,26 @@
|
||||
var _ = require('underscore');
|
||||
/**
|
||||
* Mapcard preview url generator
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
urlForStaticMap: function (mapsApiTemplate, visualization, width, height) {
|
||||
var formattedMapsApiTemplate = mapsApiTemplate.replace('{user}', visualization._permissionModel.get('owner').username);
|
||||
var template = 'tpl_' + visualization.get('id').replace(/-/g, '_');
|
||||
|
||||
var imageUrl = formattedMapsApiTemplate + '/api/v1/map/static/named/' + template + '/' + width + '/' + height + '.png' + this._generateAuthTokensParams(visualization);
|
||||
|
||||
return imageUrl;
|
||||
},
|
||||
|
||||
_generateAuthTokensParams: function (visualization) {
|
||||
var authTokens = visualization.get('auth_tokens');
|
||||
if (authTokens && authTokens.length > 0) {
|
||||
return '?' + _.map(authTokens, function (t) {
|
||||
return 'auth_token=' + t;
|
||||
}).join('&');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
var $ = require('jquery');
|
||||
|
||||
/**
|
||||
* Check if Linux user used right/middle click at the time of the event
|
||||
*
|
||||
* @param ev {Event}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isLinuxMiddleOrRightClick (ev) {
|
||||
return ev.which === 2 || ev.which === 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Mac user used CMD key at the time of the event Mac user used CMD key at the time of the event.
|
||||
*
|
||||
* @param ev {Event}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isMacCmdKeyPressed (ev) {
|
||||
return ev.metaKey;
|
||||
}
|
||||
|
||||
function isCtrlKeyPressed (ev) {
|
||||
return ev.ctrlKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for a cartodb.js view, to navigate event target's href URL through the view's router.navigate method.
|
||||
*
|
||||
* The default behavior is:
|
||||
* Unless cmd/ctrl keys are pressed it will cancel the default link behavior and instead navigate to the URL set in the
|
||||
* target's href attribute.
|
||||
*
|
||||
* Prerequisities:
|
||||
* - view has a this.router instance.
|
||||
*
|
||||
* Example of how to use:
|
||||
* - In a template:
|
||||
* <a href="/some/uri" id="#my-link" ...
|
||||
* <a href="/special/uri" id="#my-special-link" ...
|
||||
*
|
||||
* - In the view file:
|
||||
* var navigateThroughRouter = require('--/--/common/view_helpers/navigateThroughRouter');
|
||||
* module.exports = new CoreView.extend({
|
||||
* events: {
|
||||
* 'click a#my-link': navigateThroughRouter
|
||||
* 'click a#my-special-link': this._myCustomRoute
|
||||
* }
|
||||
*
|
||||
* _myCustomRoute: function(ev) {
|
||||
* // Here you can do you custom logic before/after the routing, e.g.:
|
||||
* console.log('before changing route');
|
||||
* navigateThroughRouter.apply(this, arguments);
|
||||
* console.log('after changing route');
|
||||
* }
|
||||
*
|
||||
* @param ev {Event}
|
||||
*/
|
||||
module.exports = function (ev) {
|
||||
// We always kill the default behaviour of the event, since container around view might have other click behavior.
|
||||
// In case of a cmd/ctrl click by an user.
|
||||
this.killEvent(ev);
|
||||
var url = $(ev.target).closest('a').attr('href');
|
||||
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLinuxMiddleOrRightClick(ev) && !isMacCmdKeyPressed(ev)) {
|
||||
var routerModel = (this.routerModel || this.options.routerModel || this.options.router);
|
||||
if (!routerModel) {
|
||||
throw new Error('routerModel is required');
|
||||
}
|
||||
|
||||
routerModel.navigate(url, { trigger: true });
|
||||
} else if (isCtrlKeyPressed(ev) || isMacCmdKeyPressed(ev)) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
var carto = require('carto');
|
||||
var torque = require('torque.js');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
var _ = require('underscore');
|
||||
|
||||
var ERROR_REGEX = /.*:(\d+):(\d+)\s*(.*)/;
|
||||
|
||||
var CartoParser = function (cartocss) {
|
||||
this.parse_env = null;
|
||||
this.ruleset = null;
|
||||
if (cartocss) {
|
||||
this.parse(cartocss);
|
||||
}
|
||||
};
|
||||
|
||||
CartoParser.prototype = {
|
||||
// value due to torque
|
||||
RESERVED_VARIABLES: ['mapnik-geometry-type', 'points_density', 'points_count', 'src', 'value', 'agg_value', 'agg_value_density'],
|
||||
|
||||
parse: function (cartocss) {
|
||||
this.parse_env = {
|
||||
validation_data: false,
|
||||
frames: [],
|
||||
errors: [],
|
||||
error: function (obj) {
|
||||
obj.line = carto.Parser().extractErrorLine(cartocss, obj.index);
|
||||
this.errors.push(obj);
|
||||
}
|
||||
};
|
||||
|
||||
var self = this;
|
||||
var ruleset = null;
|
||||
var defs = null;
|
||||
|
||||
try {
|
||||
// set default reference
|
||||
carto.tree.Reference.setData(carto.default_reference.version.latest);
|
||||
ruleset = (new carto.Parser(this.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 = {};
|
||||
var mapDef;
|
||||
var symbolizers;
|
||||
var i;
|
||||
var j;
|
||||
var r;
|
||||
|
||||
this.definitions = defs = ruleset.toList(this.parse_env);
|
||||
|
||||
for (i in defs) {
|
||||
if (defs[i].elements.length > 0) {
|
||||
if (defs[i].elements[0].value === 'Map') {
|
||||
mapDef = defs.splice(i, 1)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
symbolizers = torque.cartocss_reference.version.latest.layer;
|
||||
|
||||
if (mapDef) {
|
||||
mapDef.rules.forEach(function (r) {
|
||||
var key = r.name;
|
||||
var type;
|
||||
var element;
|
||||
|
||||
if (!(key in symbolizers)) {
|
||||
self.parse_env.error({
|
||||
message: 'Rule ' + key + ' not allowed for Map.',
|
||||
index: r.index
|
||||
});
|
||||
} else {
|
||||
type = symbolizers[r.name].type;
|
||||
element = r.value.value[0].value[0];
|
||||
if (!self._checkValidType(element, type)) {
|
||||
self.parse_env.error({
|
||||
message: 'Expected type ' + type + '.',
|
||||
index: r.index
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
defs = carto.inheritDefinitions(defs, this.parse_env);
|
||||
defs = carto.sortStyles(defs, this.parse_env);
|
||||
|
||||
for (i in defs) {
|
||||
for (j in defs[i]) {
|
||||
r = defs[i][j];
|
||||
if (r && r.toXML) {
|
||||
r.toXML(this.parse_env, existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toList uses parse_env.errors.message to put messages
|
||||
if (this.parse_env.errors.message) {
|
||||
_(this.parse_env.errors.message.split('\n')).each(function (m) {
|
||||
self.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 this._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'
|
||||
*
|
||||
* it could also get already parsed objects so we return them directly without parsing them again
|
||||
*/
|
||||
parseError: function (errors) {
|
||||
var parsedErrors = _.compact(errors).map(function (error) {
|
||||
if (_.isObject(error)) return error;
|
||||
|
||||
var matchedError = error.match(ERROR_REGEX);
|
||||
return matchedError
|
||||
? { line: parseInt(matchedError[1], 10), message: matchedError[3] }
|
||||
: { line: null, message: error };
|
||||
});
|
||||
|
||||
// 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) {
|
||||
function searchRecursiveByType (v, t) {
|
||||
var res = [];
|
||||
var i;
|
||||
var r;
|
||||
for (i in v) {
|
||||
if (v[i] instanceof t) {
|
||||
res.push(v[i]);
|
||||
} else if (typeof v[i] === 'object') {
|
||||
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 = [];
|
||||
var i;
|
||||
var r;
|
||||
|
||||
for (i in v) {
|
||||
if (v[i] instanceof t) {
|
||||
res.push(v[i]);
|
||||
} else if (typeof v[i] === 'object') {
|
||||
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 self = this;
|
||||
var columns = [];
|
||||
var definitions;
|
||||
var def;
|
||||
var d;
|
||||
var r;
|
||||
var f;
|
||||
var k;
|
||||
var rule;
|
||||
var columnList;
|
||||
var filter;
|
||||
var filter_key;
|
||||
|
||||
if (this.ruleset) {
|
||||
definitions = this.ruleset.toList(this.parse_env);
|
||||
for (d in definitions) {
|
||||
def = definitions[d];
|
||||
|
||||
if (def.filters) {
|
||||
// extract from rules
|
||||
for (r in def.rules) {
|
||||
rule = def.rules[r];
|
||||
columnList = method(this, rule);
|
||||
columns = columns.concat(columnList);
|
||||
}
|
||||
|
||||
if (extractVariables) {
|
||||
for (f in def.filters) {
|
||||
filter = def.filters[f];
|
||||
for (k in filter) {
|
||||
filter_key = filter[k];
|
||||
if (filter_key.key && filter_key.key.value) {
|
||||
columns.push(filter_key.key.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var colors = self._colorsFromRule(rule);
|
||||
|
||||
return _.map(colors, function (color) {
|
||||
return color.rgb;
|
||||
});
|
||||
};
|
||||
|
||||
var colors = this._extract(method, false);
|
||||
|
||||
if (opt && opt.mode === 'hex') {
|
||||
colors = _.map(colors, function (color) {
|
||||
return Utils.rgbToHex(color[0], color[1], color[2]);
|
||||
});
|
||||
}
|
||||
|
||||
return colors;
|
||||
},
|
||||
|
||||
colorsUsedForLegend: function (opt) {
|
||||
var rules = this.getDefaultRules();
|
||||
|
||||
if (rules['image-filters']) {
|
||||
var method = function (self, rule) {
|
||||
var imageFillRule = rules['image-filters'];
|
||||
var colors = self._colorsFromRule(imageFillRule);
|
||||
return _.map(colors, function (f) {
|
||||
return f.rgb;
|
||||
});
|
||||
};
|
||||
|
||||
var colors = this._extract(method, true);
|
||||
|
||||
if (opt && opt.mode === 'hex') {
|
||||
colors = _.map(colors, function (color) {
|
||||
return Utils.rgbToHex(color[0], color[1], color[2]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return _.map(colors, function (color) {
|
||||
return { color: color };
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
var i;
|
||||
var def;
|
||||
var rulesMap = {};
|
||||
|
||||
for (i = 0; i < this.definitions.length; ++i) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
for (i in rules) {
|
||||
var rule = rules[i];
|
||||
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];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CartoParser;
|
||||
@@ -0,0 +1,10 @@
|
||||
// Get coordinates from a GeoJSON
|
||||
|
||||
module.exports = function (geometry) {
|
||||
try {
|
||||
var geojson = JSON.parse(geometry);
|
||||
return geojson.coordinates.join(', ');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
var COUNT_SQL_TEMPLATE = 'select count(*) from (<%= subquery %>) st';
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
url: function () {
|
||||
return this._configModel.getSqlApiUrl();
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
opts = opts || {};
|
||||
|
||||
this._subquery = opts.subquery;
|
||||
this._configModel = opts.configModel;
|
||||
},
|
||||
|
||||
fetchCount: function (callback) {
|
||||
var opts = {};
|
||||
var count = -1;
|
||||
|
||||
opts.data = {
|
||||
api_key: this._configModel.get('api_key'),
|
||||
q: this._getCountSQL()
|
||||
};
|
||||
opts.method = this._httpMethod();
|
||||
opts.error = function (coll, resp) {
|
||||
callback && callback(count);
|
||||
};
|
||||
opts.success = function (coll, resp, options) {
|
||||
if (resp.rows && resp.rows.length) {
|
||||
count = resp.rows[0].count;
|
||||
}
|
||||
callback && callback(count);
|
||||
};
|
||||
|
||||
return Backbone.Collection.prototype.fetch.call(this, opts);
|
||||
},
|
||||
|
||||
_getCountSQL: function (excludeColumns) {
|
||||
return _.template(COUNT_SQL_TEMPLATE)({
|
||||
subquery: this._subquery
|
||||
});
|
||||
},
|
||||
|
||||
_httpMethod: function () {
|
||||
return 'GET';
|
||||
}
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = function checkAndBuildRequiredOpts (actualOpts, requiredOpts, context) {
|
||||
if (requiredOpts === void 0) {
|
||||
throw new Error('Opts are required');
|
||||
}
|
||||
|
||||
_.each(requiredOpts, function (item) {
|
||||
if (actualOpts === void 0 || actualOpts[item] === void 0) throw new Error(item + ' is required');
|
||||
context['_' + item] = actualOpts[item];
|
||||
}, context);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/* Check and perform a reset of the styles due to a change over a node definition model (if possible).
|
||||
*
|
||||
* In order to use and test this function in any view, we have removed this code
|
||||
* from user-actions Class.
|
||||
*
|
||||
* forceStyleUpdate: it will reset the styles in any case, no matter the schema or the geometry.
|
||||
* resetQueryReady: set as false ready property for query-schema-model and query-geometry-model
|
||||
* in order to wait for a node change before checking if reset styles is necessary.
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = function (nodeDefModel, layerDefModel, forceStyleUpdate, resetQueryReady) {
|
||||
if (!nodeDefModel) throw new Error('nodeDefModel is required');
|
||||
if (!layerDefModel) throw new Error('layerDefModel is required');
|
||||
|
||||
var CHANGE_READY = 'change:ready';
|
||||
var onQueryGeometryAndSchemaReady;
|
||||
var styleModel = layerDefModel.styleModel;
|
||||
var queryGeometryModel = nodeDefModel.queryGeometryModel;
|
||||
var querySchemaModel = nodeDefModel.querySchemaModel;
|
||||
|
||||
onQueryGeometryAndSchemaReady = function () {
|
||||
if (queryGeometryModel.get('ready') && querySchemaModel.get('ready')) {
|
||||
queryGeometryModel.unbind(CHANGE_READY, onQueryGeometryAndSchemaReady);
|
||||
querySchemaModel.unbind(CHANGE_READY, onQueryGeometryAndSchemaReady);
|
||||
} else {
|
||||
return; // wait until ready
|
||||
}
|
||||
|
||||
var CHANGE_STATUS = 'change:status';
|
||||
var saveDefaultStylesIfStillRelevant;
|
||||
|
||||
saveDefaultStylesIfStillRelevant = function () {
|
||||
var applyStylesAndSave = function () {
|
||||
var simpleGeometryType = queryGeometryModel.get('simple_geom');
|
||||
if (simpleGeometryType) {
|
||||
styleModel.setDefaultPropertiesByType('simple', simpleGeometryType);
|
||||
} else {
|
||||
styleModel.setDefaultPropertiesByType('none'); // fallback if there is no known geometry
|
||||
}
|
||||
layerDefModel.save();
|
||||
};
|
||||
|
||||
var styleTypeApplied = styleModel.get('type');
|
||||
|
||||
if (queryGeometryModel.isDone() && querySchemaModel.isDone()) {
|
||||
queryGeometryModel.unbind(CHANGE_STATUS, saveDefaultStylesIfStillRelevant);
|
||||
querySchemaModel.unbind(CHANGE_STATUS, saveDefaultStylesIfStillRelevant);
|
||||
} else {
|
||||
return; // wait until ready
|
||||
}
|
||||
|
||||
// If the geometry doesn't change and all columns used for style-def-model are still present + the styles are not custom
|
||||
// we can reset the style form with the geometry provided
|
||||
var haveDifferentSchema = querySchemaModel.hasDifferentSchemaThan(styleModel.getColumnsUsedForStyle());
|
||||
if (
|
||||
styleTypeApplied !== 'none' &&
|
||||
!forceStyleUpdate && (
|
||||
(!queryGeometryModel.hasChanged('simple_geom') && !haveDifferentSchema) ||
|
||||
layerDefModel.get('cartocss_custom')
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( // Only apply changes if:
|
||||
(
|
||||
layerDefModel.collection.contains(layerDefModel) && // layer still exist
|
||||
layerDefModel.get('source') === nodeDefModel.id // node is still the head of layer
|
||||
) || forceStyleUpdate // we force the style update
|
||||
) {
|
||||
applyStylesAndSave();
|
||||
}
|
||||
};
|
||||
|
||||
saveDefaultStylesIfStillRelevant();
|
||||
|
||||
if (!queryGeometryModel.isFetched()) {
|
||||
queryGeometryModel.bind(CHANGE_STATUS, saveDefaultStylesIfStillRelevant);
|
||||
}
|
||||
|
||||
if (!querySchemaModel.isFetched()) {
|
||||
querySchemaModel.bind(CHANGE_STATUS, saveDefaultStylesIfStillRelevant);
|
||||
}
|
||||
};
|
||||
|
||||
if (resetQueryReady) {
|
||||
queryGeometryModel.set('ready', false);
|
||||
querySchemaModel.set('ready', false);
|
||||
}
|
||||
|
||||
queryGeometryModel.bind(CHANGE_READY, onQueryGeometryAndSchemaReady);
|
||||
querySchemaModel.bind(CHANGE_READY, onQueryGeometryAndSchemaReady);
|
||||
onQueryGeometryAndSchemaReady();
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Checks properties from a SQL
|
||||
*/
|
||||
|
||||
var TableNameUtils = require('./table-name-utils');
|
||||
|
||||
module.exports = {
|
||||
isSameQuery: function (originalQuery, customQuery) {
|
||||
if (originalQuery == null || customQuery == null) { // eslint-disable-line
|
||||
throw new Error('Needed parameters not provided');
|
||||
}
|
||||
|
||||
var parseQuery = function (query) {
|
||||
return query
|
||||
.toLowerCase()
|
||||
.replace(/\"/g, '') // Remove quoted things like "pepe".tableName
|
||||
.replace(/;/g, '');
|
||||
};
|
||||
|
||||
return parseQuery(originalQuery) === parseQuery(customQuery);
|
||||
},
|
||||
|
||||
// return true if the sql query alters table schema in some way
|
||||
altersSchema: function (sql) {
|
||||
if (!sql) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove all line breaks in order to prevent
|
||||
// search pattern problems
|
||||
sql = this._removeLineBreaks(sql.trim());
|
||||
|
||||
return sql.search(/alter\s+[\w\."]+\s+/i) !== -1 ||
|
||||
sql.search(/drop\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^vacuum\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^create\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^reindex\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^grant\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^revoke\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^cluster\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^comment\s+on\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^explain\s+[\w\.\"]+/i) !== -1;
|
||||
},
|
||||
|
||||
// return true if the sql query alters table data
|
||||
altersData: function (sql) {
|
||||
if (!sql) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove all line breaks in order to prevent
|
||||
// search pattern problems
|
||||
sql = this._removeLineBreaks(sql.trim());
|
||||
|
||||
return this.altersSchema(sql) ||
|
||||
sql.search(/^refresh\s+materialized\s+view\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^truncate\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/insert\s+into/i) !== -1 ||
|
||||
sql.search(/update\s+[\w\.\-"]+\s+.*set/i) !== -1 ||
|
||||
sql.search(/delete\s+from/i) !== -1;
|
||||
},
|
||||
|
||||
_removeLineBreaks: function (sql) {
|
||||
return sql.replace(/\r?\n|\r/g, ' ');
|
||||
},
|
||||
|
||||
getDefaultSQL: function (tableName, userName, inOrganization) {
|
||||
return this.getDefaultSQLFromTableName(TableNameUtils.getQualifiedTableName(tableName, userName, inOrganization));
|
||||
},
|
||||
|
||||
getDefaultSQLFromTableName: function (tableName) {
|
||||
return 'SELECT * FROM ' + tableName;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
var _ = require('underscore');
|
||||
var ParserCSS = require('builder/helpers/parser-css');
|
||||
var LegendColorHelper = require('builder/editor/layers/layer-content-views/legend/form/legend-color-helper');
|
||||
|
||||
module.exports = {
|
||||
getStyleAttrs: function (styleModel) {
|
||||
if (!styleModel) return;
|
||||
|
||||
var fill = styleModel.get('fill');
|
||||
var stroke = styleModel.get('stroke');
|
||||
if (!fill && !stroke || _.isEmpty(fill) && _.isEmpty(stroke)) return;
|
||||
|
||||
return {
|
||||
fill: fill,
|
||||
stroke: stroke
|
||||
};
|
||||
},
|
||||
|
||||
getColorAttribute: function (styleModel) {
|
||||
var color = this.getColor(styleModel);
|
||||
return color && color.attribute;
|
||||
},
|
||||
|
||||
getSizeAttribute: function (styleModel) {
|
||||
var size = this.getSize(styleModel);
|
||||
return size && size.attribute;
|
||||
},
|
||||
|
||||
getColor: function (styleModel) {
|
||||
var style = this.getStyleAttrs(styleModel);
|
||||
var color = style ? (style.fill && style.fill.color || style.stroke && style.stroke.color) : null;
|
||||
return color;
|
||||
},
|
||||
|
||||
getSize: function (styleModel) {
|
||||
var style = this.getStyleAttrs(styleModel);
|
||||
var size = style ? (style.fill && style.fill.size || style.stroke && style.stroke.size) : null;
|
||||
return size;
|
||||
},
|
||||
|
||||
getColorsFromRange: function (styleModel) {
|
||||
var color = this.getColor(styleModel);
|
||||
if (!(color && color.range)) { return []; }
|
||||
|
||||
return color.range.map(function (v, index) {
|
||||
return { color: v };
|
||||
});
|
||||
},
|
||||
|
||||
getHeatmapColors: function (layerDefinitionModel) {
|
||||
var changed = layerDefinitionModel.styleModel && layerDefinitionModel.styleModel.hasChanged();
|
||||
|
||||
if (changed) {
|
||||
return this.getColorsFromRange(layerDefinitionModel.styleModel);
|
||||
}
|
||||
|
||||
var content = layerDefinitionModel.cartocssModel.get('content');
|
||||
var parser = new ParserCSS(content);
|
||||
return parser.colorsUsedForLegend({ mode: 'hex' });
|
||||
},
|
||||
|
||||
getStyleCategories: function (styleModel) {
|
||||
var color = this.getColor(styleModel);
|
||||
if (!color) { return []; }
|
||||
|
||||
if (color.range) {
|
||||
return color.range.map(function (v, index) {
|
||||
return {
|
||||
color: v,
|
||||
title: color.domain && LegendColorHelper.unquoteColor(color.domain[index]) || _t('editor.legend.legend-form.others'),
|
||||
icon: color.images && color.images[index] || ''
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (color.fixed) {
|
||||
return [{
|
||||
color: color.fixed,
|
||||
icon: color.image || '',
|
||||
title: ''
|
||||
}];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
module.exports = function () {
|
||||
var stylesWithoutAutostyles;
|
||||
|
||||
return function (layerDefModel) {
|
||||
var isAutoStyleActive = !!layerDefModel.get('autoStyle');
|
||||
|
||||
if (!isAutoStyleActive) {
|
||||
stylesWithoutAutostyles = {
|
||||
previousCartoCSS: layerDefModel.get('cartocss'),
|
||||
previousCartoCSSCustom: layerDefModel.attributes.cartocss_custom
|
||||
};
|
||||
}
|
||||
|
||||
return stylesWithoutAutostyles;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Functions to work with SQL table names
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
getUnqualifiedName: function (tablename) {
|
||||
if (!tablename) return null;
|
||||
var tk = tablename.split('.');
|
||||
if (tk.length === 2) {
|
||||
return this._getUnquotedName(tk[1]);
|
||||
}
|
||||
return this._getUnquotedName(tablename);
|
||||
},
|
||||
|
||||
getUsername: function (tablename) {
|
||||
if (!tablename) return null;
|
||||
var tk = tablename.split('.');
|
||||
if (tk.length === 2) {
|
||||
return this._getUnquotedName(tk[0]);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
_getUnquotedName: function (tablename) {
|
||||
return tablename && tablename.replace(/"/g, '');
|
||||
},
|
||||
|
||||
_quoteIfNeeded: function (name) {
|
||||
var VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_$]*$/;
|
||||
name = this._getUnquotedName(name);
|
||||
if (!VALID_IDENTIFIER.test(name)) {
|
||||
name = '"' + name + '"';
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
||||
getQualifiedTableName: function (tableName, userName, inOrganization) {
|
||||
var schemaPrefix = (inOrganization && userName) ? this._quoteIfNeeded(userName) + '.' : '';
|
||||
return schemaPrefix + this._quoteIfNeeded(this.getUnqualifiedName(tableName));
|
||||
},
|
||||
|
||||
isSameTableName: function (firstTableName, secondTableName, ownerUsername) {
|
||||
var firstParts = this._getTableNameParts(firstTableName, ownerUsername);
|
||||
var secondParts = this._getTableNameParts(secondTableName, ownerUsername);
|
||||
|
||||
return firstParts[0] === secondParts[0] && firstParts[1] === secondParts[1];
|
||||
},
|
||||
|
||||
_getTableNameParts: function (tableName, ownerUsername) {
|
||||
var table = this.getUnqualifiedName(tableName);
|
||||
var username = this.getUsername(tableName) || ownerUsername;
|
||||
|
||||
return [username, table];
|
||||
}
|
||||
};
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
var _ = require('underscore');
|
||||
var cdb = require('internal-carto.js');
|
||||
|
||||
/*
|
||||
* Util functions
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
/*
|
||||
* Simple regex to check if string is an url/ftp
|
||||
* input -> string with input text (example: 'https://carto.com')
|
||||
*
|
||||
* return -> true
|
||||
*/
|
||||
isURL: function (input) {
|
||||
var urlregex = /^((http|https|ftp)\:\/\/)/g;
|
||||
if (input) {
|
||||
return urlregex.test(input);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* check if string is blank (i.e.: str === "")
|
||||
* input -> string with input text
|
||||
*
|
||||
* @return a boolean
|
||||
*/
|
||||
isBlank: function (str) {
|
||||
return (!str || /^\s*$/.test(str));
|
||||
},
|
||||
/*
|
||||
* Transform bytes to a readable format, like MB, GB
|
||||
* input -> 34234244
|
||||
*
|
||||
* return -> 3 MB
|
||||
*/
|
||||
readablizeBytes: function (bytes, round) {
|
||||
if (!bytes || isNaN(bytes)) {
|
||||
return 0;
|
||||
}
|
||||
var s = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
var e = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
var value = (bytes / Math.pow(1024, Math.floor(e))).toFixed(2);
|
||||
|
||||
if (round) {
|
||||
value = parseInt(value, 10);
|
||||
}
|
||||
|
||||
return value + ' ' + s[e];
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert long numbers to
|
||||
* readizable numbers.
|
||||
*
|
||||
*/
|
||||
readizableNumber: function (num) {
|
||||
if (num >= 1000000000) return (num / 1000000000).toFixed(1) + 'G';
|
||||
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
||||
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
||||
return num;
|
||||
},
|
||||
|
||||
/*
|
||||
* formatNumber: adds thousands separators
|
||||
* @return a string
|
||||
*
|
||||
*/
|
||||
formatNumber: function (x) {
|
||||
if (!x) return '0';
|
||||
var parts = x.toString().split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return parts.join('.');
|
||||
},
|
||||
|
||||
/**
|
||||
* Similar to _.result, but also allows passing arbitrary arguments to the property if it's function.
|
||||
* This makes code more terse when one just wants to use a value if it's available, no if-checks required.
|
||||
*
|
||||
* @example Expected output
|
||||
* model.set('something', 'yay');
|
||||
* cdb.Utils.result(model, 'get', 'something') // => 'yay'
|
||||
* cdb.Utils.result(model, 'nonexisting', 'else') // => undefined
|
||||
* cdb.Utils.result(undefinedVar, 'get') // => null
|
||||
*
|
||||
* @example Of usage
|
||||
* return cdb.Utils.result(model, 'get', 'mightNotExist') === 'OK'
|
||||
*
|
||||
* @param {*} maybeFn
|
||||
* @return {*} Result from called maybeFn if a function, null otherwise
|
||||
*/
|
||||
result: function (object, property) {
|
||||
if (object == null) return null;
|
||||
var value = object[property];
|
||||
return _.isFunction(value) ? value.apply(object, Array.prototype.slice.call(arguments, 2)) : value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the extension of a string
|
||||
*
|
||||
*/
|
||||
getFileExtension: function (str) {
|
||||
if (!str) return '';
|
||||
return str.substr(str.lastIndexOf('.') + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add leading zeros to numbers
|
||||
*
|
||||
*/
|
||||
pad: function (num, size) {
|
||||
var s = num + '';
|
||||
while (s.length < size) s = '0' + s;
|
||||
return s;
|
||||
},
|
||||
|
||||
formatDate: function (opts) {
|
||||
if (!opts.time) throw new Error();
|
||||
if (opts.month === undefined) throw new Error();
|
||||
if (opts.year === undefined) throw new Error();
|
||||
if (!opts.day) throw new Error();
|
||||
|
||||
// Month in Date format should be specified as an index
|
||||
var month = parseInt(opts.month + 1, 10);
|
||||
var padZero = function (digit) {
|
||||
digit = String(digit);
|
||||
if (digit < 10 && digit.length === 1) {
|
||||
return '0' + digit;
|
||||
}
|
||||
return digit;
|
||||
};
|
||||
|
||||
return '' +
|
||||
opts.year + '-' +
|
||||
padZero(month) + '-' +
|
||||
padZero(opts.day) + 'T' +
|
||||
opts.time + 'Z';
|
||||
// Not adding any info about timezone
|
||||
},
|
||||
|
||||
/*
|
||||
* rgbToHex
|
||||
*
|
||||
*/
|
||||
rgbToHex: function (r, g, b) {
|
||||
function componentToHex (c) {
|
||||
var hex = c.toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||
},
|
||||
|
||||
/*
|
||||
* Returns true if the hex color passed as an input is valid
|
||||
* input -> hex (#FF00FF)
|
||||
* output -> true
|
||||
*
|
||||
* @return a boolean
|
||||
*/
|
||||
isValidHex: function (hex) {
|
||||
return !!hex.match(/(^#?[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i);
|
||||
},
|
||||
|
||||
/*
|
||||
* Returns #FFFFFF in case the input is not a valid HEX number
|
||||
*/
|
||||
sanitizeHex: function (hex) {
|
||||
if (!this.isValidHex(hex)) {
|
||||
return '#FFFFFF';
|
||||
}
|
||||
return hex;
|
||||
},
|
||||
|
||||
/*
|
||||
* Transforms an hex color into its RGB representation
|
||||
* input -> hex (#FF00FF)
|
||||
* output -> { r: 255, g: 0, b: 255 }
|
||||
*
|
||||
* @return a hash
|
||||
*/
|
||||
hexToRGB: function (hex) {
|
||||
if (!hex) {
|
||||
hex = '#FFFFFF';
|
||||
}
|
||||
|
||||
var shortRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
|
||||
hex = hex.replace(shortRegex, function (m, r, g, b) {
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
},
|
||||
/*
|
||||
* Transforms an hex color an a opacity value to a rgba string
|
||||
* input -> hex (#FF00FF) and opacity (0.4)
|
||||
* output -> 'rgba(255,0,255,0.4)'
|
||||
*
|
||||
* @return a string
|
||||
*/
|
||||
hexToRGBA: function (hex, opacity) {
|
||||
function roundToTwo (num) {
|
||||
return +(Math.round(num + 'e+2') + 'e-2');
|
||||
}
|
||||
|
||||
var rgb = this.hexToRGB(hex);
|
||||
opacity = opacity != null ? roundToTwo(opacity) : 1;
|
||||
if (rgb) {
|
||||
return 'rgba(' + [rgb.r, rgb.g, rgb.b, opacity].join(', ') + ')';
|
||||
} else {
|
||||
return hex;
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Strip html tags from a value.
|
||||
* input -> string with input text (example: '<a href="#whoknows">Jamon</a> </br> <p>Vamos</p>')
|
||||
* allowed -> allowed html tags in the result (example: '<a>')
|
||||
*
|
||||
* return -> '<a href="#whoknows">Jamon</a> Vamos'
|
||||
*/
|
||||
stripHTML: function (input, allowed) {
|
||||
allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('');
|
||||
var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
|
||||
if (!input || (typeof input !== 'string')) return '';
|
||||
return input.replace(tags, function ($0, $1) {
|
||||
return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : '';
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace all HTML characters to display HTML content in HTML
|
||||
* <h1>Hello</h1> -> <h1>Hello</h1> displays '<h1>Hello</h1>'
|
||||
*
|
||||
*/
|
||||
escapeHTML: function (str) {
|
||||
return _.escape(str);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all non-common characters like spaces, quotes, accents, etc. and
|
||||
* joins strings with underscore symbols
|
||||
*
|
||||
*/
|
||||
sanitizeString: function (str) {
|
||||
return str.replace(/[^a-z0-9\s]/gi, '').replace(/[_\s]/g, '_');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true is value is a valid number. Based on jQuery isNumeric
|
||||
*/
|
||||
isNumeric: function (value) {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats a number that to include up to 2 decimal positions if less than 10
|
||||
* and up to 1 if greater than 10.
|
||||
*/
|
||||
formatDecimalPositions: function (value) {
|
||||
// we are using here the unary operator because parseInt fails to handle exponential number
|
||||
var converted = +value;
|
||||
var p = 0;
|
||||
var abs_v;
|
||||
|
||||
if (isNaN(converted) || converted === 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
abs_v = Math.abs(converted);
|
||||
|
||||
if (abs_v > 10) {
|
||||
p = 1;
|
||||
} else if (abs_v > 0.01) {
|
||||
p = Math.min(Math.ceil(Math.abs(Math.log(abs_v) / Math.log(10))) + 2, 2);
|
||||
}
|
||||
|
||||
value = value.toFixed(p);
|
||||
var m = value.match(/(\.0+)$/);
|
||||
if (m) {
|
||||
value = value.replace(m[0], '');
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove new lines from string
|
||||
*
|
||||
*/
|
||||
removeNewLines: function (str) {
|
||||
return str.replace(/(\r\n|\n|\r)/gm, '');
|
||||
},
|
||||
|
||||
replaceLastSpaceWithNbsp: function (string) {
|
||||
var nbsp = '\u00a0';
|
||||
var lastSpace = string.lastIndexOf(' ');
|
||||
|
||||
return string.substr(0, lastSpace) + nbsp + string.substr(lastSpace + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the string ends with provided suffix
|
||||
*/
|
||||
endsWith: function (str, suffix) {
|
||||
return str.indexOf(suffix, str.length - suffix.length) !== -1;
|
||||
},
|
||||
|
||||
cloneObject: function (obj) {
|
||||
if (!obj) {
|
||||
return obj;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
|
||||
/**
|
||||
* Capitalize first letter of string
|
||||
*
|
||||
*/
|
||||
capitalize: function (str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
},
|
||||
|
||||
sanitizeHtml: function (text) {
|
||||
return cdb.core.sanitize.sanitize(text || '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the value is present
|
||||
*
|
||||
*/
|
||||
|
||||
hasValue: function (value) {
|
||||
return value !== null &&
|
||||
value !== undefined &&
|
||||
value !== '' &&
|
||||
!(typeof value === 'number' && isNaN(value));
|
||||
},
|
||||
|
||||
isValidEmail: function (email) {
|
||||
const EMAIL_REGEX = /^([^@]+)@([^@]+)\.([^@\.]+)$/i;
|
||||
|
||||
return EMAIL_REGEX.test(email);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user