Support the Relations editor for multiple selected features (close #7753)

This commit is contained in:
Quincy Morgan
2020-10-15 11:31:32 -04:00
parent 6b8384f01d
commit eaf6ff874e
6 changed files with 175 additions and 51 deletions
+10 -3
View File
@@ -73,7 +73,9 @@ en:
cancel_draw:
annotation: Canceled drawing.
change_role:
annotation: Changed the role of a relation member.
annotation:
one: Changed the role of a relation member.
other: "Changed the roles of {n} relation members."
change_tags:
annotation: Changed tags.
copy:
@@ -242,9 +244,13 @@ en:
single: This feature can't be downgraded because it has a Wikidata tag.
multiple: These features can't be downgraded because some have Wikidata tags.
add_member:
annotation: Added a member to a relation.
annotation:
one: Added a member to a relation.
other: "Added {n} members to a relation."
delete_member:
annotation: Removed a member from a relation.
annotation:
one: Removed a member from a relation.
other: "Removed {n} members from a relation."
reorder_members:
annotation: Reordered a relation's members.
connect:
@@ -683,6 +689,7 @@ en:
new_relation: New relation...
choose_relation: Choose a parent relation
role: Role
multiple_roles: Multiple Roles
choose: Select feature type
results:
one: "{n} result for {search}"
+13 -3
View File
@@ -94,7 +94,10 @@
"annotation": "Canceled drawing."
},
"change_role": {
"annotation": "Changed the role of a relation member."
"annotation": {
"one": "Changed the role of a relation member.",
"other": "Changed the roles of {n} relation members."
}
},
"change_tags": {
"annotation": "Changed tags."
@@ -323,10 +326,16 @@
}
},
"add_member": {
"annotation": "Added a member to a relation."
"annotation": {
"one": "Added a member to a relation.",
"other": "Added {n} members to a relation."
}
},
"delete_member": {
"annotation": "Removed a member from a relation."
"annotation": {
"one": "Removed a member from a relation.",
"other": "Removed {n} members from a relation."
}
},
"reorder_members": {
"annotation": "Reordered a relation's members."
@@ -882,6 +891,7 @@
"new_relation": "New relation...",
"choose_relation": "Choose a parent relation",
"role": "Role",
"multiple_roles": "Multiple Roles",
"choose": "Select feature type",
"results": {
"one": "{n} result for {search}",
+14
View File
@@ -0,0 +1,14 @@
import { actionDeleteMember } from './delete_member';
export function actionDeleteMembers(relationId, memberIndexes) {
return function(graph) {
// Remove the members in descending order so removals won't shift what members
// are at the remaining indexes
memberIndexes.sort((a, b) => b - a);
for (var i in memberIndexes) {
graph = actionDeleteMember(relationId, memberIndexes[i])(graph);
}
return graph;
};
}
+6 -2
View File
@@ -87,7 +87,9 @@ export function uiSectionRawMemberEditor(context) {
var member = { id: d.id, type: d.type, role: newRole };
context.perform(
actionChangeMember(d.relation.id, member, d.index),
t('operations.change_role.annotation')
t('operations.change_role.annotation', {
n: 1
})
);
context.validator().validate();
}
@@ -101,7 +103,9 @@ export function uiSectionRawMemberEditor(context) {
context.perform(
actionDeleteMember(d.relation.id, d.index),
t('operations.delete_member.annotation')
t('operations.delete_member.annotation', {
n: 1
})
);
if (!context.hasEntity(d.relation.id)) {
+126 -41
View File
@@ -8,7 +8,7 @@ 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 { actionDeleteMember } from '../../actions/delete_member';
import { actionDeleteMembers } from '../../actions/delete_members';
import { modeSelect } from '../../modes/select';
import { osmEntity, osmRelation } from '../../osm';
@@ -17,20 +17,18 @@ import { svgIcon } from '../../svg/icon';
import { uiCombobox } from '../combobox';
import { uiSection } from '../section';
import { uiTooltip } from '../tooltip';
import { utilArrayGroupBy, utilDisplayName, utilNoAuto, utilHighlightEntities, utilUniqueDomId } from '../../util';
import { utilArrayGroupBy, utilArrayIntersection } from '../../util/array';
import { utilDisplayName, utilNoAuto, utilHighlightEntities, utilUniqueDomId } from '../../util';
export function uiSectionRawMembershipEditor(context) {
var section = uiSection('raw-membership-editor', context)
.shouldDisplay(function() {
return _entityIDs && _entityIDs.length === 1;
return _entityIDs && _entityIDs.length;
})
.label(function() {
var entity = context.hasEntity(_entityIDs[0]);
if (!entity) return '';
var parents = context.graph().parentRelations(entity);
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 });
@@ -52,6 +50,72 @@ export function uiSectionRawMembershipEditor(context) {
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 = utilUniqueDomId('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();
@@ -76,14 +140,28 @@ export function uiSectionRawMembershipEditor(context) {
if (d === 0) return; // called on newrow (shouldn't happen)
if (_inChange) return; // avoid accidental recursive call #5731
var oldRole = d.member.role;
var newRole = context.cleanRelationRole(d3_select(this).property('value'));
if (oldRole !== newRole) {
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(
actionChangeMember(d.relation.id, Object.assign({}, d.member, { role: newRole }), d.index),
t('operations.change_role.annotation')
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();
}
@@ -95,12 +173,22 @@ export function uiSectionRawMembershipEditor(context) {
this.blur(); // avoid keeping focus on the button
_showBlank = false;
var member = { id: _entityIDs[0], type: context.entity(_entityIDs[0]).type, role: role };
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(
actionAddMember(d.relation.id, member),
t('operations.add_member.annotation')
actionAddMembers(d.relation.id, _entityIDs, role),
t('operations.add_member.annotation', {
n: _entityIDs.length
})
);
context.validator().validate();
@@ -108,7 +196,7 @@ export function uiSectionRawMembershipEditor(context) {
var relation = osmRelation();
context.perform(
actionAddEntity(relation),
actionAddMember(relation.id, member),
actionAddMembers(relation.id, _entityIDs, role),
t('operations.add.annotation.relation')
);
// changing the mode also runs `validate`
@@ -124,9 +212,15 @@ export function uiSectionRawMembershipEditor(context) {
// remove the hover-highlight styling
utilHighlightEntities([d.relation.id], false, context);
var indexes = d.members.map(function(member) {
return member.index;
});
context.perform(
actionDeleteMember(d.relation.id, d.index),
t('operations.delete_member.annotation')
actionDeleteMembers(d.relation.id, indexes),
t('operations.delete_member.annotation', {
n: _entityIDs.length
})
);
context.validator().validate();
}
@@ -197,25 +291,7 @@ export function uiSectionRawMembershipEditor(context) {
function renderDisclosureContent(selection) {
var entityID = _entityIDs[0];
var entity = context.entity(entityID);
var parents = context.graph().parentRelations(entity);
var memberships = [];
parents.slice(0, _maxMemberships).forEach(function(relation) {
relation.members.forEach(function(member, index) {
if (member.id === entity.id) {
memberships.push({
relation: relation,
member: member,
index: index,
domId: utilUniqueDomId(entityID + '-membership-' + relation.id + '-' + index)
});
}
});
});
var memberships = getMemberships();
var list = selection.selectAll('.member-list')
.data([0]);
@@ -228,7 +304,7 @@ export function uiSectionRawMembershipEditor(context) {
var items = list.selectAll('li.member-row-normal')
.data(memberships, function(d) {
return osmEntity.key(d.relation) + ',' + d.index;
return d.hash;
});
items.exit()
@@ -299,9 +375,19 @@ export function uiSectionRawMembershipEditor(context) {
return d.domId;
})
.property('type', 'text')
.attr('placeholder', t('inspector.role'))
.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)
.property('value', function(d) { return d.member.role; })
.on('blur', changeRole)
.on('change', changeRole);
@@ -309,7 +395,6 @@ export function uiSectionRawMembershipEditor(context) {
wrapEnter.each(bindTypeahead);
}
var newMembership = list.selectAll('.member-row-new')
.data(_showBlank ? [0] : []);
@@ -450,7 +535,7 @@ export function uiSectionRawMembershipEditor(context) {
taginfo.roles({
debounce: true,
rtype: rtype || '',
geometry: context.graph().geometry(entityID),
geometry: context.graph().geometry(_entityIDs[0]),
query: role
}, function(err, data) {
if (!err) callback(sort(role, data));
+6 -2
View File
@@ -65,7 +65,9 @@ export function validationMissingRole() {
onClick: function(context) {
context.perform(
actionDeleteMember(this.issue.entityIds[0], this.issue.data.member.index),
t('operations.delete_member.annotation')
t('operations.delete_member.annotation', {
n: 1
})
);
}
})
@@ -93,7 +95,9 @@ export function validationMissingRole() {
var member = { id: this.issue.entityIds[1], type: oldMember.type, role: role };
context.perform(
actionChangeMember(this.issue.entityIds[0], member, oldMember.index),
t('operations.change_role.annotation')
t('operations.change_role.annotation', {
n: 1
})
);
}
});