UNPKG

tangram

Version:
541 lines (449 loc) 17.6 kB
import StyleParser from './style_parser'; import { compileFunctionStrings } from '../utils/functions'; import log from '../utils/log'; import mergeObjects from '../utils/merge'; import Geo from '../utils/geo'; import { buildFilter } from './filter'; // N.B.: 'visible' is legacy compatibility for 'enabled' const reserved = ['filter', 'draw', 'visible', 'enabled', 'data', 'exclusive', 'priority']; let layer_cache = {}; export function layerCache () { return layer_cache; } function cacheKey (layers) { if (layers.length > 1) { var k = layers[0]; for (var i=1; i < layers.length; i++) { k += '/' + layers[i]; } return k; } return layers[0]; } // Merge matching layer trees into a final draw group export function mergeTrees(matchingTrees, group) { let draws, treeDepth = 0; // Find deepest tree for (let t=0; t < matchingTrees.length; t++) { if (matchingTrees[t].length > treeDepth) { treeDepth = matchingTrees[t].length; } } // No layers to parse if (treeDepth === 0) { return null; } // Merged draw group object let draw = { visible: true, // visible by default }; // Iterate trees in parallel for (let x=0; x < treeDepth; x++) { // Pull out the requested draw group, for each tree, at this depth (avoiding duplicates at the same level in tree) draws = []; matchingTrees.forEach(tree => { if (tree[x] && tree[x][group] && draws.indexOf(tree[x][group]) === -1) { draws.push(tree[x][group]); } }); if (draws.length === 0) { continue; } // Merge draw objects mergeObjects(draw, ...draws); } // Short-circuit if not visible if (draw.visible === false) { return null; } return draw; } const blacklist = ['any', 'all', 'not', 'none']; class Layer { constructor({ layer, name, parent, draw, visible, enabled, filter, exclusive, priority, styles }) { this.id = Layer.id++; this.config_data = layer.data; this.parent = parent; this.name = name; this.full_name = this.parent ? (this.parent.full_name + ':' + this.name) : this.name; this.draw = draw; this.filter = filter; this.exclusive = (exclusive === true); this.priority = (priority != null ? priority : Number.MAX_SAFE_INTEGER); this.styles = styles; this.is_built = false; enabled = (enabled === undefined) ? visible : enabled; // `visible` property is backwards compatible for `enabled` this.enabled = (enabled !== false); // layer is enabled unless explicitly set to disabled // Denormalize layer name to draw groups if (this.draw) { for (let group in this.draw) { this.draw[group] = (this.draw[group] == null) ? {} : this.draw[group]; if (typeof this.draw[group] !== 'object') { // Invalid draw group let msg = `Draw group '${group}' for layer ${this.full_name} is invalid, must be an object, `; msg += `but was set to \`${group}: ${this.draw[group]}\` instead`; log('warn', msg); // TODO: fire external event that clients to subscribe to delete this.draw[group]; } } } } build () { log('trace', `Building layer '${this.full_name}'`); this.buildFilter(); this.buildDraw(); this.is_built = true; } buildDraw() { this.draw = compileFunctionStrings(this.draw, StyleParser.wrapFunction); this.calculatedDraw = calculateDraw(this); } buildFilter() { this.filter_original = this.filter; this.filter = compileFunctionStrings(this.filter, StyleParser.wrapFunction); let type = typeof this.filter; if (this.filter != null && type !== 'object' && type !== 'function') { // Invalid filter let msg = `Filter for layer ${this.full_name} is invalid, filter value must be an object or function, `; msg += `but was set to \`filter: ${this.filter}\` instead`; log('warn', msg); // TODO: fire external event that clients to subscribe to return; } try { this.buildZooms(); this.buildPropMatches(); if (this.filter != null && (typeof this.filter === 'function' || Object.keys(this.filter).length > 0)) { this.filter = buildFilter(this.filter, FilterOptions); } else { this.filter = null; } } catch(e) { // Invalid filter let msg = `Filter for layer ${this.full_name} is invalid, \`filter: ${JSON.stringify(this.filter)}\` `; msg += `failed with error '${e.message}', stack trace: ${e.stack}`; log('warn', msg); // TODO: fire external event that clients to subscribe to } } // Zooms often cull large swaths of the layer tree, so they get special treatment and are checked first buildZooms() { let zoom = this.filter && this.filter.$zoom; // has an explicit zoom filter let ztype = typeof zoom; if (zoom != null) { this.zooms = {}; if (ztype === 'number') { this.zooms[zoom] = true; } else if (Array.isArray(zoom)) { for (let z=0; z < zoom.length; z++) { this.zooms[zoom[z]] = true; } } else if (ztype === 'object' && (zoom.min != null || zoom.max != null)) { let zmin = zoom.min || 0; let zmax = zoom.max || Geo.max_style_zoom; for (let z=zmin; z < zmax; z++) { this.zooms[z] = true; } } delete this.filter.$zoom; // don't process zoom through usual generic filter logic } } buildPropMatches() { if (!this.filter || Array.isArray(this.filter) || typeof this.filter === 'function') { return; } Object.keys(this.filter).forEach(key => { if (blacklist.indexOf(key) === -1) { let val = this.filter[key]; let type = typeof val; let array = Array.isArray(val); if (!(array || type === 'string' || type === 'number')) { return; } if (key[0] === '$') { // Context property this.context_prop_matches = this.context_prop_matches || []; this.context_prop_matches.push([key.substring(1), array ? val : [val]]); delete this.filter[key]; } else if (key.indexOf('.') === -1) { // exclude nested feature properties // Single-level feature property this.feature_prop_matches = this.feature_prop_matches || []; this.feature_prop_matches.push([key, array ? val : [val]]); delete this.filter[key]; } } }); } doPropMatches (context) { if (this.feature_prop_matches) { for (let r=0; r < this.feature_prop_matches.length; r++) { let match = this.feature_prop_matches[r]; let val = context.feature.properties[match[0]]; if (val == null || match[1].indexOf(val) === -1) { return false; } } } if (this.context_prop_matches) { for (let r=0; r < this.context_prop_matches.length; r++) { let match = this.context_prop_matches[r]; let val = context[match[0]]; if (val == null || match[1].indexOf(val) === -1) { return false; } } } return true; } doesMatch (context) { if (!this.enabled) { return false; } if (!this.is_built) { this.build(); } // zoom pre-filter: skip rest of filter if out of layer zoom range if (this.zooms != null && !this.zooms[context.zoom]) { return false; } // direct feature property matches if (!this.doPropMatches(context)) { return false; } // any remaining filter (more complex matches or dynamic function) let match; if (this.filter instanceof Function){ try { match = this.filter(context); } catch (error) { // Filter function error let msg = `Filter for this ${this.full_name}: \`filter: ${this.filter_original}\` `; msg += `failed with error '${error.message}', stack trace: ${error.stack}`; log('error', msg, context.feature); } } else { match = this.filter == null; } if (match) { if (this.children_to_parse) { parseLayerChildren(this, this.children_to_parse, this.styles); delete this.children_to_parse; } return true; } return false; } } Layer.id = 0; export class LayerLeaf extends Layer { constructor (config) { super(config); this.is_leaf = true; } } export class LayerTree extends Layer { constructor (config) { super(config); this.is_tree = true; this.layers = config.layers || []; } addLayer (layer) { this.layers.push(layer); } buildDrawGroups (context) { let layers = [], layer_ids = []; matchFeature(context, [this], layers, layer_ids); if (layers.length > 0) { let cache_key = cacheKey(layer_ids); // Only evaluate each layer combination once (undefined means not yet evaluated, // null means evaluated with no draw object) if (layer_cache[cache_key] === undefined) { // Find all the unique visible draw blocks for this layer tree let draw_groups = layers.map(x => x && x.visible !== false && x.calculatedDraw); let draw_keys = {}; for (let r=0; r < draw_groups.length; r++) { let stack = draw_groups[r]; if (!stack) { continue; } for (let g=0; g < stack.length; g++) { let group = stack[g]; for (let key in group) { draw_keys[key] = true; } } } // Calculate each draw group for (let draw_key in draw_keys) { layer_cache[cache_key] = layer_cache[cache_key] || {}; layer_cache[cache_key][draw_key] = mergeTrees(draw_groups, draw_key); // Only save the ones that weren't null if (!layer_cache[cache_key][draw_key]) { delete layer_cache[cache_key][draw_key]; } else { layer_cache[cache_key][draw_key].key = cache_key + '/' + draw_key; layer_cache[cache_key][draw_key].layers = layers.map(x => x && x.full_name); layer_cache[cache_key][draw_key].group = draw_key; } } // No layers evaluated if (layer_cache[cache_key] && Object.keys(layer_cache[cache_key]).length === 0) { layer_cache[cache_key] = null; } } return layer_cache[cache_key]; } } } export const FilterOptions = { // Handle unit conversions on filter ranges rangeTransform(val) { if (typeof val === 'string' && val.trim().slice(-3) === 'px2') { return `${parseFloat(val)} * context.meters_per_pixel_sq`; } return val; } }; export function isReserved(key) { return reserved.indexOf(key) > -1; } function isEmpty(obj) { return Object.keys(obj).length === 0; } export function groupProps(obj) { let reserved = {}, children = {}; for (let key in obj) { if (isReserved(key)) { reserved[key] = obj[key]; } else { children[key] = obj[key]; } } return [reserved, children]; } export function calculateDraw(layer) { let draw = []; if (layer.parent) { let cs = layer.parent.calculatedDraw || []; draw.push(...cs); } draw.push(layer.draw); return draw; } export function parseLayerNode(name, layer, parent, styles) { layer = (layer == null) ? {} : layer; let properties = { name, layer, parent, styles }; let [reserved, children] = groupProps(layer); let empty = isEmpty(children); let Create; if (empty && parent != null) { Create = LayerLeaf; } else { Create = LayerTree; } let r = new Create(Object.assign(properties, reserved)); // only process child layers if this layer is enabled if (r.enabled) { if (parent) { parent.addLayer(r); } r.children_to_parse = empty ? null : children; } return r; } function parseLayerChildren (parent, children, styles) { for (let key in children) { let child = children[key]; if (typeof child === 'object' && !Array.isArray(child)) { parseLayerNode(key, child, parent, styles); } else { // Invalid layer let msg = `Layer value must be an object: cannot create layer '${key}: ${JSON.stringify(child)}'`; msg += `, under parent layer '${parent.full_name}'.`; // If the parent is a style name, this may be an incorrectly nested layer if (styles[parent.name]) { msg += ` The parent name '${parent.name}' is also the name of a style, did you mean to create a 'draw' group`; if (parent.parent) { msg += ` under '${parent.parent.name}'`; } msg += ' instead?'; } log('warn', msg); // TODO: fire external event that clients to subscribe to } } // Sort sub-layers so they are applied deterministically when multiple layers modify the same properties // Sort order is: exclusive layers first, then by explicit layer priority, then by layer name parent.layers.sort((a, b) => { // Exclusive layers come first // If an exclusive layer matches, no further sibling layers are matched if (a.exclusive < b.exclusive) return 1; else if (a.exclusive > b.exclusive) return -1; // When sub-sorting exclusive layers, sort the higher priority layers first, since only one exlcusive layer // can match and the first one that matches should be the highest priority. // When sub-sorting non-exclusive layers, sort the lower priority layers first, since multiple layers may // match, and when they are merged in order, the later layers will overwrite the earlier ones -- so we want // the higher priority ones to match last so that they "win". const direction = (a.exclusive ? 1 : -1); // Sub-sort by explicit priority if (a.priority > b.priority) return direction; else if (a.priority < b.priority) return -direction; // Sub-sort by layer name as last resort if (a.full_name < b.full_name) return direction; else if (a.full_name > b.full_name) return -direction; }); } export function parseLayers (layers, styles) { layer_cache = {}; // clear layer cache let layer_trees = {}; for (let key in layers) { let layer = layers[key]; if (layer) { layer_trees[key] = parseLayerNode(key, layer, null, styles); } } return layer_trees; } export function matchFeature(context, layers, collected_layers, collected_layers_ids) { let matched = false; let child_matched = false; if (layers.length === 0) { return; } for (let r=0; r < layers.length; r++) { let current = layers[r]; if (current.is_leaf) { if (current.doesMatch(context)) { matched = true; collected_layers.push(current); collected_layers_ids.push(current.id); if (current.exclusive) { break; // only one exclusive layer can match, stop matching further sibling layers } } } else if (current.is_tree) { if (current.doesMatch(context)) { matched = true; child_matched = matchFeature( context, current.layers, collected_layers, collected_layers_ids ); if (!child_matched) { collected_layers.push(current); collected_layers_ids.push(current.id); } if (current.exclusive) { break; // only one exclusive layer can match, stop matching further sibling layers } } } } return matched; }