modules/renderer/features.js (354 lines of code) (raw):
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { osmEntity } from '../osm';
import { utilRebind } from '../util/rebind';
import { groupManager } from '../entities/group_manager';
import { utilArrayGroupBy, utilArrayUnion, utilQsString, utilStringQs } from '../util';
import { t } from '../util/locale';
export function rendererFeatures(context) {
var dispatch = d3_dispatch('change', 'redraw');
var features = utilRebind({}, dispatch, 'on');
var _deferred = new Set();
var _cullFactor = 1;
var _cache = {};
var _rules = {};
var _rulesArray = [];
var _stats = {};
var _keys = [];
var _hidden = [];
var _forceVisible = {};
function update() {
if (!window.mocha) {
var q = utilStringQs(window.location.hash.substring(1));
var disabled = features.disabled();
if (disabled.length) {
q.disable_features = disabled.join(',');
} else {
delete q.disable_features;
}
window.location.replace('#' + utilQsString(q, true));
context.storage('disabled-features', disabled.join(','));
}
_hidden = features.hidden();
dispatch.call('change');
dispatch.call('redraw');
}
function defineRule(k, filter, title, description, max) {
var isEnabled = true;
_keys.push(k);
_rules[k] = {
key: k,
title: title,
description: description,
filter: filter,
enabled: isEnabled, // whether the user wants it enabled..
count: 0,
currentMax: (max || Infinity),
defaultMax: (max || Infinity),
enable: function() { this.enabled = true; this.currentMax = this.defaultMax; },
disable: function() { this.enabled = false; this.currentMax = 0; },
hidden: function() {
return !context.editableDataEnabled() ||
(this.count === 0 && !this.enabled) ||
this.count > this.currentMax * _cullFactor;
},
autoHidden: function() { return this.hidden() && this.currentMax > 0; }
};
_rulesArray.push(_rules[k]);
}
for (var id in groupManager.toggleableGroups) {
var group = groupManager.toggleableGroups[id];
defineRule(group.basicID(), group.matchesTags, group.localizedName(), group.localizedDescription(), group.toggleableMax());
}
// Lines or areas that don't match another feature filter.
// IMPORTANT: The 'others' feature must be the last one defined,
// so that code in getMatches can skip this test if `hasMatch = true`
defineRule('others', function isOther(tags, geometry) {
return (geometry === 'line' || geometry === 'area');
}, t('feature.others.description'), t('feature.others.tooltip'));
features.featuresArray = function() {
return _rulesArray;
};
features.features = function() {
return _rules;
};
features.keys = function() {
return _keys;
};
features.enabled = function(k) {
if (!arguments.length) {
return _keys.filter(function(k) { return _rules[k].enabled; });
}
return _rules[k] && _rules[k].enabled;
};
features.disabled = function(k) {
if (!arguments.length) {
return _keys.filter(function(k) { return !_rules[k].enabled; });
}
return _rules[k] && !_rules[k].enabled;
};
features.hidden = function(k) {
if (!arguments.length) {
return _keys.filter(function(k) { return _rules[k].hidden(); });
}
return _rules[k] && _rules[k].hidden();
};
features.autoHidden = function(k) {
if (!arguments.length) {
return _keys.filter(function(k) { return _rules[k].autoHidden(); });
}
return _rules[k] && _rules[k].autoHidden();
};
features.enable = function(k) {
if (_rules[k] && !_rules[k].enabled) {
_rules[k].enable();
update();
}
};
features.enableAll = function() {
var didEnable = false;
for (var k in _rules) {
if (!_rules[k].enabled) {
didEnable = true;
_rules[k].enable();
}
}
if (didEnable) update();
};
features.disable = function(k) {
if (_rules[k] && _rules[k].enabled) {
_rules[k].disable();
update();
}
};
features.disableAll = function() {
var didDisable = false;
for (var k in _rules) {
if (_rules[k].enabled) {
didDisable = true;
_rules[k].disable();
}
}
if (didDisable) update();
};
features.toggle = function(k) {
if (_rules[k]) {
(function(f) { return f.enabled ? f.disable() : f.enable(); }(_rules[k]));
update();
}
};
features.resetStats = function() {
for (var i = 0; i < _keys.length; i++) {
_rules[_keys[i]].count = 0;
}
dispatch.call('change');
};
features.gatherStats = function(d, resolver, dimensions) {
var needsRedraw = false;
var types = utilArrayGroupBy(d, 'type');
var entities = [].concat(types.relation || [], types.way || [], types.node || []);
var currHidden, geometry, matches, i, j;
for (i = 0; i < _keys.length; i++) {
_rules[_keys[i]].count = 0;
}
// adjust the threshold for point/building culling based on viewport size..
// a _cullFactor of 1 corresponds to a 1000x1000px viewport..
_cullFactor = dimensions[0] * dimensions[1] / 1000000;
for (i = 0; i < entities.length; i++) {
geometry = entities[i].geometry(resolver);
matches = Object.keys(features.getMatches(entities[i], resolver, geometry));
for (j = 0; j < matches.length; j++) {
_rules[matches[j]].count++;
}
}
currHidden = features.hidden();
if (currHidden !== _hidden) {
_hidden = currHidden;
needsRedraw = true;
dispatch.call('change');
}
return needsRedraw;
};
features.stats = function() {
for (var i = 0; i < _keys.length; i++) {
_stats[_keys[i]] = _rules[_keys[i]].count;
}
return _stats;
};
features.clear = function(d) {
for (var i = 0; i < d.length; i++) {
features.clearEntity(d[i]);
}
};
features.clearEntity = function(entity) {
delete _cache[osmEntity.key(entity)];
};
features.reset = function() {
Array.from(_deferred).forEach(function(handle) {
window.cancelIdleCallback(handle);
_deferred.delete(handle);
});
_cache = {};
};
// only certain relations are worth checking
function relationShouldBeChecked(relation) {
// multipolygon features have `area` geometry and aren't checked here
return relation.tags.type === 'boundary';
}
features.getMatches = function(entity, resolver, geometry) {
if (geometry === 'vertex' ||
(geometry === 'relation' && !relationShouldBeChecked(entity))) return {};
var ent = osmEntity.key(entity);
if (!_cache[ent]) {
_cache[ent] = {};
}
if (!_cache[ent].matches) {
var matches = {};
var hasMatch = false;
for (var i = 0; i < _keys.length; i++) {
if (_keys[i] === 'others') {
if (hasMatch) continue;
// If an entity...
// 1. is a way that hasn't matched other 'interesting' feature rules,
if (entity.type === 'way') {
var parents = features.getParents(entity, resolver, geometry);
// 2a. belongs only to a single multipolygon relation
if ((parents.length === 1 && parents[0].isMultipolygon()) ||
// 2b. or belongs only to boundary relations
(parents.length > 0 && parents.every(function(parent) { return parent.tags.type === 'boundary'; }))) {
// ...then match whatever feature rules the parent relation has matched.
// see #2548, #2887
//
// IMPORTANT:
// For this to work, getMatches must be called on relations before ways.
//
var pkey = osmEntity.key(parents[0]);
if (_cache[pkey] && _cache[pkey].matches) {
matches = Object.assign({}, _cache[pkey].matches); // shallow copy
continue;
}
}
}
}
if (_rules[_keys[i]].filter(entity.tags, geometry)) {
matches[_keys[i]] = hasMatch = true;
}
}
_cache[ent].matches = matches;
}
return _cache[ent].matches;
};
features.getParents = function(entity, resolver, geometry) {
if (geometry === 'point') return [];
var ent = osmEntity.key(entity);
if (!_cache[ent]) {
_cache[ent] = {};
}
if (!_cache[ent].parents) {
var parents = [];
if (geometry === 'vertex') {
parents = resolver.parentWays(entity);
} else { // 'line', 'area', 'relation'
parents = resolver.parentRelations(entity);
}
_cache[ent].parents = parents;
}
return _cache[ent].parents;
};
features.isHiddenPreset = function(preset, geometry) {
if (!_hidden.length) return false;
if (!preset.tags) return false;
var test = preset.setTags({}, geometry);
for (var key in _rules) {
if (_rules[key].filter(test, geometry)) {
if (_hidden.indexOf(key) !== -1) {
return _rules[key];
}
return false;
}
}
return false;
};
features.isHiddenFeature = function(entity, resolver, geometry) {
if (!_hidden.length) return false;
if (!entity.version) return false;
if (_forceVisible[entity.id]) return false;
var matches = Object.keys(features.getMatches(entity, resolver, geometry));
return matches.length && matches.every(function(k) { return features.hidden(k); });
};
features.isHiddenChild = function(entity, resolver, geometry) {
if (!_hidden.length) return false;
if (!entity.version || geometry === 'point') return false;
if (_forceVisible[entity.id]) return false;
var parents = features.getParents(entity, resolver, geometry);
if (!parents.length) return false;
for (var i = 0; i < parents.length; i++) {
if (!features.isHidden(parents[i], resolver, parents[i].geometry(resolver))) {
return false;
}
}
return true;
};
features.hasHiddenConnections = function(entity, resolver) {
if (!_hidden.length) return false;
var childNodes, connections;
if (entity.type === 'midpoint') {
childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];
connections = [];
} else {
childNodes = entity.nodes ? resolver.childNodes(entity) : [];
connections = features.getParents(entity, resolver, entity.geometry(resolver));
}
// gather ways connected to child nodes..
connections = childNodes.reduce(function(result, e) {
return resolver.isShared(e) ? utilArrayUnion(result, resolver.parentWays(e)) : result;
}, connections);
return connections.some(function(e) {
return features.isHidden(e, resolver, e.geometry(resolver));
});
};
features.isHidden = function(entity, resolver, geometry) {
if (!_hidden.length) return false;
if (!entity.version) return false;
var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature);
return fn(entity, resolver, geometry);
};
features.filter = function(d, resolver) {
if (!_hidden.length) return d;
var result = [];
for (var i = 0; i < d.length; i++) {
var entity = d[i];
if (!features.isHidden(entity, resolver, entity.geometry(resolver))) {
result.push(entity);
}
}
return result;
};
features.forceVisible = function(entityIDs) {
if (!arguments.length) return Object.keys(_forceVisible);
_forceVisible = {};
for (var i = 0; i < entityIDs.length; i++) {
_forceVisible[entityIDs[i]] = true;
var entity = context.hasEntity(entityIDs[i]);
if (entity && entity.type === 'relation') {
// also show relation members (one level deep)
for (var j in entity.members) {
_forceVisible[entity.members[j].id] = true;
}
}
}
return features;
};
features.init = function() {
var storage = context.storage('disabled-features');
if (storage) {
var storageDisabled = storage.replace(/;/g, ',').split(',');
storageDisabled.forEach(features.disable);
}
var q = utilStringQs(window.location.hash.substring(1));
if (q.disable_features) {
var hashDisabled = q.disable_features.replace(/;/g, ',').split(',');
hashDisabled.forEach(features.disable);
}
};
// warm up the feature matching cache upon merging fetched data
context.history().on('merge.features', function(newEntities) {
if (!newEntities) return;
var handle = window.requestIdleCallback(function() {
var graph = context.graph();
var types = utilArrayGroupBy(newEntities, 'type');
// ensure that getMatches is called on relations before ways
var entities = [].concat(types.relation || [], types.way || [], types.node || []);
for (var i = 0; i < entities.length; i++) {
var geometry = entities[i].geometry(graph);
features.getMatches(entities[i], graph, geometry);
}
});
_deferred.add(handle);
});
return features;
}