2017-11-24 08:43:18 +01:00
|
|
|
/*
|
|
|
|
termap - Terminal Map Viewer
|
|
|
|
by Michael Strassburger <codepoet@cpan.org>
|
|
|
|
|
|
|
|
Minimalistic parser and compiler for Mapbox (Studio) Map Style files
|
|
|
|
See: https://www.mapbox.com/mapbox-gl-style-spec/
|
|
|
|
|
|
|
|
Compiles layer filter instructions into a chain of true/false returning
|
|
|
|
anonymous functions to improve rendering speed compared to realtime parsing.
|
|
|
|
*/
|
2017-11-24 10:42:09 +01:00
|
|
|
'use strict';
|
2017-11-24 08:43:18 +01:00
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
2017-11-24 10:42:09 +01:00
|
|
|
class Styler {
|
2017-11-24 08:43:18 +01:00
|
|
|
constructor(file) {
|
|
|
|
this.styleById = {};
|
|
|
|
this.styleByLayer = {};
|
|
|
|
var base, name;
|
|
|
|
const json = JSON.parse(fs.readFileSync(file).toString());
|
|
|
|
this.styleName = json.name;
|
|
|
|
if (json.constants) {
|
|
|
|
this._replaceConstants(json.constants, json.layers);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const style of json.layers) {
|
|
|
|
if (style.ref && this.styleById[style.ref]) {
|
|
|
|
for (const ref of ['type', 'source-layer', 'minzoom', 'maxzoom', 'filter']) {
|
|
|
|
if (this.styleById[style.ref][ref] && !style[ref]) {
|
|
|
|
style[ref] = this.styleById[style.ref][ref];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
style.appliesTo = this._compileFilter(style.filter);
|
|
|
|
|
|
|
|
//TODO Better translation of: @styleByLayer[style['source-layer']] ?= []
|
|
|
|
if ((base = this.styleByLayer)[name = style['source-layer']] == null) {
|
|
|
|
base[name] = [];
|
|
|
|
}
|
|
|
|
this.styleByLayer[style['source-layer']].push(style);
|
|
|
|
this.styleById[style.id] = style;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-23 09:16:01 +01:00
|
|
|
getStyleFor(layer, feature) {
|
2017-11-24 08:43:18 +01:00
|
|
|
if (!this.styleByLayer[layer]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const style of this.styleByLayer[layer]) {
|
|
|
|
if (style.appliesTo(feature)) {
|
|
|
|
return style;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
_replaceConstants(constants, tree) {
|
|
|
|
for (const id in tree) {
|
|
|
|
const node = tree[id];
|
|
|
|
switch (typeof node) {
|
|
|
|
case 'object':
|
|
|
|
if (node.constructor.name.match(/Stream/)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
this._replaceConstants(constants, node);
|
|
|
|
break;
|
|
|
|
case 'string':
|
|
|
|
if (node.charAt(0) === '@') {
|
|
|
|
tree[id] = constants[node];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//TODO Better translation of the long cases.
|
|
|
|
_compileFilter(filter) {
|
2017-11-24 09:01:05 +01:00
|
|
|
let filters;
|
2017-11-24 08:43:18 +01:00
|
|
|
switch (filter != null ? filter[0] : void 0) {
|
|
|
|
case 'all':
|
|
|
|
filter = filter.slice(1);
|
|
|
|
filters = (() => {
|
|
|
|
return filter.map((sub) => this._compileFilter(sub));
|
|
|
|
}).call(this);
|
2017-11-24 09:01:05 +01:00
|
|
|
return (feature) => !!filters.find((appliesTo) => {
|
|
|
|
return !appliesTo(feature);
|
|
|
|
});
|
2017-11-24 08:43:18 +01:00
|
|
|
case 'any':
|
|
|
|
filter = filter.slice(1);
|
|
|
|
filters = (() => {
|
|
|
|
return filter.map((sub) => this._compileFilter(sub));
|
|
|
|
}).call(this);
|
2017-11-24 09:01:05 +01:00
|
|
|
return (feature) => !!filters.find((appliesTo) => {
|
|
|
|
return appliesTo(feature);
|
|
|
|
});
|
2017-11-24 08:43:18 +01:00
|
|
|
case 'none':
|
|
|
|
filter = filter.slice(1);
|
|
|
|
filters = (() => {
|
|
|
|
return filter.map((sub) => this._compileFilter(sub));
|
|
|
|
}).call(this);
|
2017-11-24 09:01:05 +01:00
|
|
|
return (feature) => !filters.find((appliesTo) => {
|
|
|
|
return !appliesTo(feature);
|
|
|
|
});
|
2017-11-24 08:43:18 +01:00
|
|
|
case '==':
|
|
|
|
return (feature) => feature.properties[filter[1]] === filter[2];
|
2017-12-23 09:07:42 +01:00
|
|
|
case '!=':
|
2017-11-24 08:43:18 +01:00
|
|
|
return (feature) => feature.properties[filter[1]] !== filter[2];
|
2017-12-23 09:07:42 +01:00
|
|
|
case 'in':
|
2017-11-24 09:01:05 +01:00
|
|
|
return (feature) => !!filter.slice(2).find((value) => {
|
|
|
|
return feature.properties[filter[1]] === value;
|
|
|
|
});
|
2017-11-24 08:43:18 +01:00
|
|
|
case '!in':
|
2017-11-24 09:01:05 +01:00
|
|
|
return (feature) => !filter.slice(2).find((value) => {
|
|
|
|
return feature.properties[filter[1]] === value;
|
|
|
|
});
|
2017-12-23 09:07:42 +01:00
|
|
|
case 'has':
|
2017-11-24 08:43:18 +01:00
|
|
|
return (feature) => !!feature.properties[filter[1]];
|
2017-12-23 09:07:42 +01:00
|
|
|
case '!has':
|
2017-11-24 08:43:18 +01:00
|
|
|
return (feature) => !feature.properties[filter[1]];
|
2017-12-23 09:07:42 +01:00
|
|
|
case '>':
|
2017-11-24 08:43:18 +01:00
|
|
|
return (feature) => feature.properties[filter[1]] > filter[2];
|
2017-12-23 09:07:42 +01:00
|
|
|
case '>=':
|
2017-11-24 08:43:18 +01:00
|
|
|
return (feature) => feature.properties[filter[1]] >= filter[2];
|
2017-12-23 09:07:42 +01:00
|
|
|
case '<':
|
2017-11-24 08:43:18 +01:00
|
|
|
return (feature) => feature.properties[filter[1]] < filter[2];
|
2017-12-23 09:07:42 +01:00
|
|
|
case '<=':
|
2017-11-24 08:43:18 +01:00
|
|
|
return (feature) => feature.properties[filter[1]] <= filter[2];
|
|
|
|
default:
|
|
|
|
return () => true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-11-24 10:42:09 +01:00
|
|
|
|
|
|
|
module.exports = Styler;
|