modules/ui/sections/raw_membership_editor.js (442 lines of code) (raw):
import { select as d3_select } from 'd3-selection';
import { utilArrayGroupBy, utilArrayIntersection, utilUniqueString } from '@id-sdk/util';
import { presetManager } from '../../presets';
import { t, localizer } from '../../core/localizer';
import { actionAddEntity } from '../../actions/add_entity';
import { actionAddMember } from '../../actions/add_member';
import { actionChangeMember } from '../../actions/change_member';
import { actionDeleteMembers } from '../../actions/delete_members';
import { modeSelect } from '../../modes/select';
import { osmEntity, osmRelation } from '../../osm';
import { services } from '../../services';
import { svgIcon } from '../../svg/icon';
import { uiCombobox } from '../combobox';
import { uiSection } from '../section';
import { uiTooltip } from '../tooltip';
import { utilDisplayName, utilNoAuto, utilHighlightEntities } from '../../util';
export function uiSectionRawMembershipEditor(context) {
var section = uiSection('raw-membership-editor', context)
.shouldDisplay(function() {
return _entityIDs && _entityIDs.length;
})
.label(function() {
var parents = getSharedParentRelations();
var gt = parents.length > _maxMemberships ? '>' : '';
var count = gt + parents.slice(0, _maxMemberships).length;
return t('inspector.title_count', { title: t.html('inspector.relations'), count: count });
})
.disclosureContent(renderDisclosureContent);
var taginfo = services.taginfo;
var nearbyCombo = uiCombobox(context, 'parent-relation')
.minItems(1)
.fetcher(fetchNearbyRelations)
.itemsMouseEnter(function(d3_event, d) {
if (d.relation) utilHighlightEntities([d.relation.id], true, context);
})
.itemsMouseLeave(function(d3_event, d) {
if (d.relation) utilHighlightEntities([d.relation.id], false, context);
});
var _inChange = false;
var _entityIDs = [];
var _showBlank;
var _maxMemberships = 1000;
function getSharedParentRelations() {
var parents = [];
for (var i = 0; i < _entityIDs.length; i++) {
var entity = context.graph().hasEntity(_entityIDs[i]);
if (!entity) continue;
if (i === 0) {
parents = context.graph().parentRelations(entity);
} else {
parents = utilArrayIntersection(parents, context.graph().parentRelations(entity));
}
if (!parents.length) break;
}
return parents;
}
function getMemberships() {
var memberships = [];
var relations = getSharedParentRelations().slice(0, _maxMemberships);
var isMultiselect = _entityIDs.length > 1;
var i, relation, membership, index, member, indexedMember;
for (i = 0; i < relations.length; i++) {
relation = relations[i];
membership = {
relation: relation,
members: [],
hash: osmEntity.key(relation)
};
for (index = 0; index < relation.members.length; index++) {
member = relation.members[index];
if (_entityIDs.indexOf(member.id) !== -1) {
indexedMember = Object.assign({}, member, { index: index });
membership.members.push(indexedMember);
membership.hash += ',' + index.toString();
if (!isMultiselect) {
// For single selections, list one entry per membership per relation.
// For multiselections, list one entry per relation.
memberships.push(membership);
membership = {
relation: relation,
members: [],
hash: osmEntity.key(relation)
};
}
}
}
if (membership.members.length) memberships.push(membership);
}
memberships.forEach(function(membership) {
membership.domId = utilUniqueString('membership-' + membership.relation.id);
var roles = [];
membership.members.forEach(function(member) {
if (roles.indexOf(member.role) === -1) roles.push(member.role);
});
membership.role = roles.length === 1 ? roles[0] : roles;
});
return memberships;
}
function selectRelation(d3_event, d) {
d3_event.preventDefault();
// remove the hover-highlight styling
utilHighlightEntities([d.relation.id], false, context);
context.enter(modeSelect(context, [d.relation.id]));
}
function zoomToRelation(d3_event, d) {
d3_event.preventDefault();
var entity = context.entity(d.relation.id);
context.map().zoomToEase(entity);
// highlight the relation in case it wasn't previously on-screen
utilHighlightEntities([d.relation.id], true, context);
}
function changeRole(d3_event, d) {
if (d === 0) return; // called on newrow (shouldn't happen)
if (_inChange) return; // avoid accidental recursive call #5731
var newRole = context.cleanRelationRole(d3_select(this).property('value'));
if (!newRole.trim() && typeof d.role !== 'string') return;
var membersToUpdate = d.members.filter(function(member) {
return member.role !== newRole;
});
if (membersToUpdate.length) {
_inChange = true;
context.perform(
function actionChangeMemberRoles(graph) {
membersToUpdate.forEach(function(member) {
var newMember = Object.assign({}, member, { role: newRole });
delete newMember.index;
graph = actionChangeMember(d.relation.id, newMember, member.index)(graph);
});
return graph;
},
t('operations.change_role.annotation', {
n: membersToUpdate.length
})
);
context.validator().validate();
}
_inChange = false;
}
function addMembership(d, role) {
this.blur(); // avoid keeping focus on the button
_showBlank = false;
function actionAddMembers(relationId, ids, role) {
return function(graph) {
for (var i in ids) {
var member = { id: ids[i], type: graph.entity(ids[i]).type, role: role };
graph = actionAddMember(relationId, member)(graph);
}
return graph;
};
}
if (d.relation) {
context.perform(
actionAddMembers(d.relation.id, _entityIDs, role),
t('operations.add_member.annotation', {
n: _entityIDs.length
})
);
context.validator().validate();
} else {
var relation = osmRelation();
context.perform(
actionAddEntity(relation),
actionAddMembers(relation.id, _entityIDs, role),
t('operations.add.annotation.relation')
);
// changing the mode also runs `validate`
context.enter(modeSelect(context, [relation.id]).newFeature(true));
}
}
function deleteMembership(d3_event, d) {
this.blur(); // avoid keeping focus on the button
if (d === 0) return; // called on newrow (shouldn't happen)
// remove the hover-highlight styling
utilHighlightEntities([d.relation.id], false, context);
var indexes = d.members.map(function(member) {
return member.index;
});
context.perform(
actionDeleteMembers(d.relation.id, indexes),
t('operations.delete_member.annotation', {
n: _entityIDs.length
})
);
context.validator().validate();
}
function fetchNearbyRelations(q, callback) {
var newRelation = {
relation: null,
value: t('inspector.new_relation'),
display: t.html('inspector.new_relation')
};
var entityID = _entityIDs[0];
var result = [];
var graph = context.graph();
function baseDisplayLabel(entity) {
var matched = presetManager.match(entity, graph);
var presetName = (matched && matched.name()) || t('inspector.relation');
var entityName = utilDisplayName(entity) || '';
return presetName + ' ' + entityName;
}
var explicitRelation = q && context.hasEntity(q.toLowerCase());
if (explicitRelation && explicitRelation.type === 'relation' && explicitRelation.id !== entityID) {
// loaded relation is specified explicitly, only show that
result.push({
relation: explicitRelation,
value: baseDisplayLabel(explicitRelation) + ' ' + explicitRelation.id
});
} else {
context.history().intersects(context.map().extent()).forEach(function(entity) {
if (entity.type !== 'relation' || entity.id === entityID) return;
var value = baseDisplayLabel(entity);
if (q && (value + ' ' + entity.id).toLowerCase().indexOf(q.toLowerCase()) === -1) return;
result.push({ relation: entity, value: value });
});
result.sort(function(a, b) {
return osmRelation.creationOrder(a.relation, b.relation);
});
// Dedupe identical names by appending relation id - see #2891
var dupeGroups = Object.values(utilArrayGroupBy(result, 'value'))
.filter(function(v) { return v.length > 1; });
dupeGroups.forEach(function(group) {
group.forEach(function(obj) {
obj.value += ' ' + obj.relation.id;
});
});
}
result.forEach(function(obj) {
obj.title = obj.value;
});
result.unshift(newRelation);
callback(result);
}
function renderDisclosureContent(selection) {
var memberships = getMemberships();
var list = selection.selectAll('.member-list')
.data([0]);
list = list.enter()
.append('ul')
.attr('class', 'member-list')
.merge(list);
var items = list.selectAll('li.member-row-normal')
.data(memberships, function(d) {
return d.hash;
});
items.exit()
.each(unbind)
.remove();
// Enter
var itemsEnter = items.enter()
.append('li')
.attr('class', 'member-row member-row-normal form-field');
// highlight the relation in the map while hovering on the list item
itemsEnter.on('mouseover', function(d3_event, d) {
utilHighlightEntities([d.relation.id], true, context);
})
.on('mouseout', function(d3_event, d) {
utilHighlightEntities([d.relation.id], false, context);
});
var labelEnter = itemsEnter
.append('label')
.attr('class', 'field-label')
.attr('for', function(d) {
return d.domId;
});
var labelLink = labelEnter
.append('span')
.attr('class', 'label-text')
.append('a')
.attr('href', '#')
.on('click', selectRelation);
labelLink
.append('span')
.attr('class', 'member-entity-type')
.html(function(d) {
var matched = presetManager.match(d.relation, context.graph());
return (matched && matched.name()) || t('inspector.relation');
});
labelLink
.append('span')
.attr('class', 'member-entity-name')
.html(function(d) {
const matched = presetManager.match(d.relation, context.graph());
// hide the network from the name if there is NSI match
return utilDisplayName(d.relation, matched.suggestion);
});
labelEnter
.append('button')
.attr('class', 'remove member-delete')
.call(svgIcon('#iD-operation-delete'))
.on('click', deleteMembership);
labelEnter
.append('button')
.attr('class', 'member-zoom')
.attr('title', t('icons.zoom_to'))
.call(svgIcon('#iD-icon-framed-dot', 'monochrome'))
.on('click', zoomToRelation);
var wrapEnter = itemsEnter
.append('div')
.attr('class', 'form-field-input-wrap form-field-input-member');
wrapEnter
.append('input')
.attr('class', 'member-role')
.attr('id', function(d) {
return d.domId;
})
.property('type', 'text')
.property('value', function(d) {
return typeof d.role === 'string' ? d.role : '';
})
.attr('title', function(d) {
return Array.isArray(d.role) ? d.role.filter(Boolean).join('\n') : d.role;
})
.attr('placeholder', function(d) {
return Array.isArray(d.role) ? t('inspector.multiple_roles') : t('inspector.role');
})
.classed('mixed', function(d) {
return Array.isArray(d.role);
})
.call(utilNoAuto)
.on('blur', changeRole)
.on('change', changeRole);
if (taginfo) {
wrapEnter.each(bindTypeahead);
}
var newMembership = list.selectAll('.member-row-new')
.data(_showBlank ? [0] : []);
// Exit
newMembership.exit()
.remove();
// Enter
var newMembershipEnter = newMembership.enter()
.append('li')
.attr('class', 'member-row member-row-new form-field');
var newLabelEnter = newMembershipEnter
.append('label')
.attr('class', 'field-label');
newLabelEnter
.append('input')
.attr('placeholder', t('inspector.choose_relation'))
.attr('type', 'text')
.attr('class', 'member-entity-input')
.call(utilNoAuto);
newLabelEnter
.append('button')
.attr('class', 'remove member-delete')
.call(svgIcon('#iD-operation-delete'))
.on('click', function() {
list.selectAll('.member-row-new')
.remove();
});
var newWrapEnter = newMembershipEnter
.append('div')
.attr('class', 'form-field-input-wrap form-field-input-member');
newWrapEnter
.append('input')
.attr('class', 'member-role')
.property('type', 'text')
.attr('placeholder', t('inspector.role'))
.call(utilNoAuto);
// Update
newMembership = newMembership
.merge(newMembershipEnter);
newMembership.selectAll('.member-entity-input')
.on('blur', cancelEntity) // if it wasn't accepted normally, cancel it
.call(nearbyCombo
.on('accept', acceptEntity)
.on('cancel', cancelEntity)
);
// Container for the Add button
var addRow = selection.selectAll('.add-row')
.data([0]);
// enter
var addRowEnter = addRow.enter()
.append('div')
.attr('class', 'add-row');
var addRelationButton = addRowEnter
.append('button')
.attr('class', 'add-relation');
addRelationButton
.call(svgIcon('#iD-icon-plus', 'light'));
addRelationButton
.call(uiTooltip().title(t.html('inspector.add_to_relation')).placement(localizer.textDirection() === 'ltr' ? 'right' : 'left'));
addRowEnter
.append('div')
.attr('class', 'space-value'); // preserve space
addRowEnter
.append('div')
.attr('class', 'space-buttons'); // preserve space
// update
addRow = addRow
.merge(addRowEnter);
addRow.select('.add-relation')
.on('click', function() {
_showBlank = true;
section.reRender();
list.selectAll('.member-entity-input').node().focus();
});
function acceptEntity(d) {
if (!d) {
cancelEntity();
return;
}
// remove hover-higlighting
if (d.relation) utilHighlightEntities([d.relation.id], false, context);
var role = context.cleanRelationRole(list.selectAll('.member-row-new .member-role').property('value'));
addMembership(d, role);
}
function cancelEntity() {
var input = newMembership.selectAll('.member-entity-input');
input.property('value', '');
// remove hover-higlighting
context.surface().selectAll('.highlighted')
.classed('highlighted', false);
}
function bindTypeahead(d) {
var row = d3_select(this);
var role = row.selectAll('input.member-role');
var origValue = role.property('value');
function sort(value, data) {
var sameletter = [];
var other = [];
for (var i = 0; i < data.length; i++) {
if (data[i].value.substring(0, value.length) === value) {
sameletter.push(data[i]);
} else {
other.push(data[i]);
}
}
return sameletter.concat(other);
}
role.call(uiCombobox(context, 'member-role')
.fetcher(function(role, callback) {
var rtype = d.relation.tags.type;
taginfo.roles({
debounce: true,
rtype: rtype || '',
geometry: context.graph().geometry(_entityIDs[0]),
query: role
}, function(err, data) {
if (!err) callback(sort(role, data));
});
})
.on('cancel', function() {
role.property('value', origValue);
})
);
}
function unbind() {
var row = d3_select(this);
row.selectAll('input.member-role')
.call(uiCombobox.off, context);
}
}
section.entityIDs = function(val) {
if (!arguments.length) return _entityIDs;
_entityIDs = val;
_showBlank = false;
return section;
};
return section;
}