Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

Eval-less filter implementation #11

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 78 additions & 65 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,9 @@
'use strict';

var VectorTileFeatureTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
module.exports = createFilter;

function infix(operator) {
return function(_, key, value) {
if (key === '$type') {
return 't' + operator + VectorTileFeatureTypes.indexOf(value);
} else {
return 'p[' + JSON.stringify(key) + ']' + operator + JSON.stringify(value);
}
};
}

function strictInfix(operator) {
var nonstrictInfix = infix(operator);
return function(_, key, value) {
if (key === '$type') {
return nonstrictInfix(_, key, value);
} else {
return 'typeof(p[' + JSON.stringify(key) + ']) === typeof(' + JSON.stringify(value) + ') && ' +
nonstrictInfix(_, key, value);
}
};
}

var operators = {
'==': infix('==='),
'!=': infix('!=='),
'>': strictInfix('>'),
'<': strictInfix('<'),
'<=': strictInfix('<='),
'>=': strictInfix('>='),
'in': function(_, key) {
return '(function(){' + Array.prototype.slice.call(arguments, 2).map(function(value) {
return 'if (' + operators['=='](_, key, value) + ') return true;';
}).join('') + 'return false;})()';
},
'!in': function() {
return '!(' + operators.in.apply(this, arguments) + ')';
},
'any': function() {
return Array.prototype.slice.call(arguments, 1).map(function(filter) {
return '(' + compile(filter) + ')';
}).join('||') || 'false';
},
'all': function() {
return Array.prototype.slice.call(arguments, 1).map(function(filter) {
return '(' + compile(filter) + ')';
}).join('&&') || 'true';
},
'none': function() {
return '!(' + operators.any.apply(this, arguments) + ')';
}
};

function compile(filter) {
return operators[filter[0]].apply(filter, filter);
}

function truth() {
return true;
}
var types = ['Unknown', 'Point', 'LineString', 'Polygon'];
var typeLookup = {'Point': 1, 'LineString': 2, 'Polygon': 3};

/**
* Given a filter expressed as nested arrays, return a new function
Expand All @@ -70,9 +13,79 @@ function truth() {
* @param {Array} filter mapbox gl filter
* @returns {Function} filter-evaluating function
*/
module.exports = function (filter) {
function createFilter(filter) {
if (!filter) return truth;
var filterStr = 'var p = f.properties || f.tags || {}, t = f.type; return ' + compile(filter) + ';';
// jshint evil: true
return new Function('f', filterStr);
};

var op = filter[0];
var key = filter[1];

var val =
op === '==' ||
op === '!=' ||
op === '>' ||
op === '<' ||
op === '<=' ||
op === '>=' ? filter[2] :
op === 'in' ||
op === '!in' ? filter.slice(2) : filter.slice(1);

var opFn =
op === 'in' ? (key === '$type' ? inType : inNormal) :
op === '!in' ? (key === '$type' ? notInType : notIn) :
op === '==' ? (key === '$type' ? equalsType : equals) :
op === '!=' ? (key === '$type' ? notEqualsType : notEquals) :
op === '<' ? lesser :
op === '>' ? greater :
op === '<=' ? lesserEqual :
op === '>=' ? greaterEqual :
val.map(createFilter);

if (op === 'any') return createAny(opFn);
if (op === 'all') return createAll(opFn);
if (op === 'none') return createNone(opFn);
return createOp(opFn, key, val);
}

function createOp(opFn, key, val) {
return function(f) {
return opFn(f.properties || f.tags || {}, f.type, key, val);
};
}
function createAny(opFn) {
return function(f) {
for (var i = 0; i < opFn.length; i++) {
if (opFn[i](f)) return true;
}
return false;
};
}
function createAll(opFn) {
return function(f) {
for (var i = 0; i < opFn.length; i++) {
if (!opFn[i](f)) return false;
}
return true;
};
}
function createNone(opFn) {
return function(f) {
for (var i = 0; i < opFn.length; i++) {
if (opFn[i](f)) return false;
}
return true;
};
}

function inNormal(p, t, key, val) { return val.indexOf(p[key]) !== -1; }
function inType(p, t, key, val) { return val.indexOf(types[t]) !== -1; }
function notIn(p, t, key, val) { return val.indexOf(p[key]) === -1; }
function notInType(p, t, key, val) { return val.indexOf(types[t]) === -1; }
function equals(p, t, key, val) { return p[key] === val; }
function equalsType(p, t, key, val) { return t === typeLookup[val]; }
function notEquals(p, t, key, val) { return p[key] !== val; }
function notEqualsType(p, t, key, val) { return t !== typeLookup[val]; }
function lesser(p, t, key, val) { return typeof p[key] === typeof val && p[key] < val; }
function greater(p, t, key, val) { return typeof p[key] === typeof val && p[key] > val; }
function lesserEqual(p, t, key, val) { return typeof p[key] === typeof val && p[key] <= val; }
function greaterEqual(p, t, key, val) { return typeof p[key] === typeof val && p[key] >= val; }
function truth() { return true; }
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@
"description": "Creates filtering function for vector tile features",
"dependencies": {},
"devDependencies": {
"eslint": "^1.10.3",
"eslint-config-mourner": "^1.0.1",
"tape": "^3.2.1"
},
"scripts": {
"pretest": "eslint index.js test.js",
"test": "tape test.js"
},
"eslintConfig": {
"extends": "mourner",
"rules": {
"space-before-function-paren": [2, "never"]
}
},
"repository": {
"type": "git",
"url": "git@github.com:mapbox/feature-filter.git"
Expand Down