Files
iD/modules/ui/field.js
Bryan Housel 2b2a71f597 Don't pre-resolve and index complex locationSets into GeoJSON.
This was taking a lot of time at app startup.

Instad now we resolve and index only the include and exclude parts.
We can still determine the valid locationSets at runtime in `locationSetsAt()`
by checking the `_locationIncludedIn` and `_locationExcludedIn` caches.

This also upgrades the locationManger to an ES6 class.

This also includes some hacky code in nsi.js so that the NSI will continue to work.
The NSI matcher can build its own location index, but it doesn't need to do this.
We monkeypatch a few of the matcher collections to work with the new LocationManager.
2022-10-28 10:49:01 -04:00

355 lines
11 KiB
JavaScript

import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { t, localizer } from '../core/localizer';
import { locationManager } from '../core/LocationManager';
import { svgIcon } from '../svg/icon';
import { uiTooltip } from './tooltip';
import { geoExtent } from '../geo/extent';
import { uiFieldHelp } from './field_help';
import { uiFields } from './fields';
import { uiTagReference } from './tag_reference';
import { utilRebind, utilUniqueDomId } from '../util';
export function uiField(context, presetField, entityIDs, options) {
options = Object.assign({
show: true,
wrap: true,
remove: true,
revert: true,
info: true
}, options);
var dispatch = d3_dispatch('change', 'revert');
var field = Object.assign({}, presetField); // shallow copy
field.domId = utilUniqueDomId('form-field-' + field.safeid);
var _show = options.show;
var _state = '';
var _tags = {};
var _entityExtent;
if (entityIDs && entityIDs.length) {
_entityExtent = entityIDs.reduce(function(extent, entityID) {
var entity = context.graph().entity(entityID);
return extent.extend(entity.extent(context.graph()));
}, geoExtent());
}
var _locked = false;
var _lockedTip = uiTooltip()
.title(() => t.append('inspector.lock.suggestion', { label: field.title }))
.placement('bottom');
field.keys = field.keys || [field.key];
// only create the fields that are actually being shown
if (_show && !field.impl) {
createField();
}
// Creates the field.. This is done lazily,
// once we know that the field will be shown.
function createField() {
field.impl = uiFields[field.type](field, context)
.on('change', function(t, onInput) {
dispatch.call('change', field, t, onInput);
});
if (entityIDs) {
field.entityIDs = entityIDs;
// if this field cares about the entities, pass them along
if (field.impl.entityIDs) {
field.impl.entityIDs(entityIDs);
}
}
}
function isModified() {
if (!entityIDs || !entityIDs.length) return false;
return entityIDs.some(function(entityID) {
var original = context.graph().base().entities[entityID];
var latest = context.graph().entity(entityID);
return field.keys.some(function(key) {
return original ? latest.tags[key] !== original.tags[key] : latest.tags[key];
});
});
}
function tagsContainFieldKey() {
return field.keys.some(function(key) {
if (field.type === 'multiCombo') {
for (var tagKey in _tags) {
if (tagKey.indexOf(key) === 0) {
return true;
}
}
return false;
}
return _tags[key] !== undefined;
});
}
function revert(d3_event, d) {
d3_event.stopPropagation();
d3_event.preventDefault();
if (!entityIDs || _locked) return;
dispatch.call('revert', d, d.keys);
}
function remove(d3_event, d) {
d3_event.stopPropagation();
d3_event.preventDefault();
if (_locked) return;
var t = {};
d.keys.forEach(function(key) {
t[key] = undefined;
});
dispatch.call('change', d, t);
}
field.render = function(selection) {
var container = selection.selectAll('.form-field')
.data([field]);
// Enter
var enter = container.enter()
.append('div')
.attr('class', function(d) { return 'form-field form-field-' + d.safeid; })
.classed('nowrap', !options.wrap);
if (options.wrap) {
var labelEnter = enter
.append('label')
.attr('class', 'field-label')
.attr('for', function(d) { return d.domId; });
var textEnter = labelEnter
.append('span')
.attr('class', 'label-text');
textEnter
.append('span')
.attr('class', 'label-textvalue')
.each(function(d) { d.label()(d3_select(this)); });
textEnter
.append('span')
.attr('class', 'label-textannotation');
if (options.remove) {
labelEnter
.append('button')
.attr('class', 'remove-icon')
.attr('title', t('icons.remove'))
.call(svgIcon('#iD-operation-delete'));
}
if (options.revert) {
labelEnter
.append('button')
.attr('class', 'modified-icon')
.attr('title', t('icons.undo'))
.call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-redo' : '#iD-icon-undo'));
}
}
// Update
container = container
.merge(enter);
container.select('.field-label > .remove-icon') // propagate bound data
.on('click', remove);
container.select('.field-label > .modified-icon') // propagate bound data
.on('click', revert);
container
.each(function(d) {
var selection = d3_select(this);
if (!d.impl) {
createField();
}
var reference, help;
// instantiate field help
if (options.wrap && field.type === 'restrictions') {
help = uiFieldHelp(context, 'restrictions');
}
// instantiate tag reference
if (options.wrap && options.info) {
var referenceKey = d.key || '';
if (d.type === 'multiCombo') { // lookup key without the trailing ':'
referenceKey = referenceKey.replace(/:$/, '');
}
reference = uiTagReference(d.reference || { key: referenceKey }, context);
if (_state === 'hover') {
reference.showing(false);
}
}
selection
.call(d.impl);
// add field help components
if (help) {
selection
.call(help.body)
.select('.field-label')
.call(help.button);
}
// add tag reference components
if (reference) {
selection
.call(reference.body)
.select('.field-label')
.call(reference.button);
}
d.impl.tags(_tags);
});
container
.classed('locked', _locked)
.classed('modified', isModified())
.classed('present', tagsContainFieldKey());
// show a tip and lock icon if the field is locked
var annotation = container.selectAll('.field-label .label-textannotation');
var icon = annotation.selectAll('.icon')
.data(_locked ? [0]: []);
icon.exit()
.remove();
icon.enter()
.append('svg')
.attr('class', 'icon')
.append('use')
.attr('xlink:href', '#fas-lock');
container.call(_locked ? _lockedTip : _lockedTip.destroy);
};
field.state = function(val) {
if (!arguments.length) return _state;
_state = val;
return field;
};
field.tags = function(val) {
if (!arguments.length) return _tags;
_tags = val;
if (tagsContainFieldKey() && !_show) {
// always show a field if it has a value to display
_show = true;
if (!field.impl) {
createField();
}
}
return field;
};
field.locked = function(val) {
if (!arguments.length) return _locked;
_locked = val;
return field;
};
field.show = function() {
_show = true;
if (!field.impl) {
createField();
}
if (field.default && field.key && _tags[field.key] !== field.default) {
var t = {};
t[field.key] = field.default;
dispatch.call('change', this, t);
}
};
// A shown field has a visible UI, a non-shown field is in the 'Add field' dropdown
field.isShown = function() {
return _show;
};
// An allowed field can appear in the UI or in the 'Add field' dropdown.
// A non-allowed field is hidden from the user altogether
field.isAllowed = function() {
if (entityIDs &&
entityIDs.length > 1 &&
uiFields[field.type].supportsMultiselection === false) return false;
if (field.geometry && !entityIDs.every(function(entityID) {
return field.matchGeometry(context.graph().geometry(entityID));
})) return false;
if (entityIDs && _entityExtent && field.locationSetID) { // is field allowed in this location?
var validHere = locationManager.locationSetsAt(_entityExtent.center());
if (!validHere[field.locationSetID]) return false;
}
var prerequisiteTag = field.prerequisiteTag;
if (entityIDs &&
!tagsContainFieldKey() && // ignore tagging prerequisites if a value is already present
prerequisiteTag) {
if (!entityIDs.every(function(entityID) {
var entity = context.graph().entity(entityID);
if (prerequisiteTag.key) {
var value = entity.tags[prerequisiteTag.key];
if (!value) return false;
if (prerequisiteTag.valueNot) {
return prerequisiteTag.valueNot !== value;
}
if (prerequisiteTag.value) {
return prerequisiteTag.value === value;
}
} else if (prerequisiteTag.keyNot) {
if (entity.tags[prerequisiteTag.keyNot]) return false;
}
return true;
})) return false;
}
return true;
};
field.focus = function() {
if (field.impl) {
field.impl.focus();
}
};
return utilRebind(field, dispatch, 'on');
}