///
///
///
///
namespace dSeeder {
let _generationLimit = 100;
function _getWithParentIds(data: Member[], id: number | null): Member {
const member = data.find((member) => member.id === id);
if (member === undefined) {
throw new Error(`Member with id (${id}) was not found`);
}
return member;
}
function _getWithoutParentIds(data: Member[], id: number | null): Member {
let member = _getWithParentIds(data, id);
member.parent1Id = null;
member.parent2Id = null;
return member;
}
function _get(data: Member[], ids: number[], options: { preserveParentIds: boolean }): Member[] {
const members = new Array();
ids.forEach(id => {
const member = (options.preserveParentIds)
? _getWithParentIds(data, id)
: _getWithoutParentIds(data, id);
members.push(member);
});
return members;
}
function _getChildren(data: Member[], ...parents: Member[]): Member[] {
const childIds = data.filter((member) =>
parents.some((parent) => parent.id === member.parent1Id || parent.id === member.parent2Id))
.map((member) => member.id);
if (childIds.length === 0) {
return [];
}
const children = _get(data, childIds, { preserveParentIds: true });
const parentDepthOffset = parents.find((parent) => parent.depthOffset !== undefined)?.depthOffset;
if (parentDepthOffset !== undefined) {
children.forEach((child) => child.depthOffset = parentDepthOffset + 1);
}
return children;
}
function _getOtherParents(data: Member[], children: Member[], ...parents: Member[]): Member[] {
const parentIds = parents.map((parent) => parent.id);
const otherParentIds = children.map((child) =>
parentIds.includes(child.parent1Id as number)
? child.parent2Id
: child.parent1Id);
// remove duplicates when siblings have common parent
const uniqueOtherParentIds = otherParentIds.filter((value, index) =>
index === otherParentIds.indexOf(value));
// remove parentIds so their ancestors aren't included
const otherParents = _get(data, uniqueOtherParentIds as number[], { preserveParentIds: false });
const parentDepthOffset = parents.find((parent) => parent.depthOffset !== undefined)?.depthOffset;
if (parentDepthOffset !== undefined) {
otherParents.forEach((otherParent) => otherParent.depthOffset = parentDepthOffset);
}
return otherParents;
}
function _getRelatives(data: Member[], targetId?: number): Member[] {
if (data.length === 0) {
throw new Error("Data cannot be empty");
}
if (targetId === undefined) {
throw new Error("TargetId cannot be undefined");
}
// start at 1, as specificed by dTree
const depthOffsetStart = 1;
const members = new Array();
const target = _getWithParentIds(data, targetId);
const hasParent1 = target.parent1Id !== null;
const hasParent2 = target.parent2Id !== null;
if (!hasParent1 && !hasParent2) {
target.depthOffset = depthOffsetStart;
}
else {
target.depthOffset = depthOffsetStart + 1;
const parentIds = new Array();
if (hasParent1) {
parentIds.push(target.parent1Id as number);
}
if (hasParent2) {
parentIds.push(target.parent2Id as number);
}
// remove parentIds so their ancestors aren't included
const parents = _get(data, parentIds, { preserveParentIds: false });
parents.forEach((parent) => parent.depthOffset = depthOffsetStart);
members.push(...parents);
const siblingIds = data.filter((member) =>
((member.parent1Id === target.parent1Id || member.parent2Id === target.parent2Id)
|| (member.parent1Id === target.parent2Id || member.parent2Id === target.parent1Id))
&& member.id !== target.id).map((member) => member.id);
const siblings = _get(data, siblingIds, { preserveParentIds: true });
siblings.forEach((sibling) => sibling.depthOffset = depthOffsetStart + 1);
members.push(...siblings);
}
members.push(target);
const children = _getChildren(data, target);
members.push(...children);
if (children.length === 0) {
return members;
}
const otherParents = _getOtherParents(data, children, target);
members.push(...otherParents);
let nextGeneration = children;
do {
const nextGenerationChildren = _getChildren(data, ...nextGeneration);
members.push(...nextGenerationChildren);
const nextGenerationOtherParents = _getOtherParents(data, nextGenerationChildren, ...nextGeneration);
members.push(...nextGenerationOtherParents);
nextGeneration = nextGenerationChildren;
} while (nextGeneration.length > 0);
return members;
}
function _combineIntoMarriages(data: Member[], options?: SeederOptions): TreeNode[] {
if (data.length === 1) {
return data.map((member) => new TreeNode(member));
}
let parentGroups = data.map((member) => {
return [member.parent1Id, member.parent2Id].filter((id) => id !== null);
}).filter((group) => group.length > 0);
parentGroups = [...new Set(parentGroups
// sort and stringify for comparison without iterating over each element
.map((group) => JSON.stringify(group.sort())))]
// parse elements back into objects
.map((group) => JSON.parse(group));
if (parentGroups.length === 0) {
throw new Error("At least one member must have at least one parent");
}
const treeNodes = new Array();
parentGroups.forEach((currentParentGroup) => {
const nodeId = currentParentGroup[0];
const node = new TreeNode(_getWithParentIds(data, nodeId), options);
const nodeMarriages = parentGroups.filter((group) => group.includes(nodeId));
nodeMarriages.forEach((marriedCouple) => {
// remove marriage to prevent interating on it twice,
// except if it's currently being iterated by the outer forEach
// removing that would cause the next one to replaced as current and be skipped
const index = parentGroups.indexOf(marriedCouple);
if (index !== parentGroups.indexOf(currentParentGroup)) {
parentGroups.splice(index, 1);
}
const marriage = new TreeNodeMarriage();
const spouseId = marriedCouple[1];
if (spouseId !== undefined) {
marriage.spouse = new TreeNode(_getWithParentIds(data, spouseId), options);
}
marriage.children = data.filter((member) => {
if (member.parent1Id !== null && member.parent2Id !== null) {
return marriedCouple.includes(member.parent1Id as number)
&& marriedCouple.includes(member.parent2Id as number);
}
if (member.parent1Id !== null && member.parent2Id === null) {
return marriedCouple.includes(member.parent1Id as number)
&& member.parent2Id === null;
}
if (member.parent1Id === null && member.parent2Id !== null) {
return marriedCouple.includes(member.parent2Id as number)
&& member.parent1Id === null;
}
return false;
}).map((child) => new TreeNode(child, options));
node.marriages.push(marriage);
});
treeNodes.push(node);
});
return treeNodes;
}
function _coalesce(data: TreeNode[]): TreeNode[] {
if (data.length === 0) {
throw new Error("Data cannot be empty");
}
if (data.length === 1) {
return data;
}
let count = 0;
while (data.length > 1) {
for (let index = 0; index < data.length; index++) {
const node = data[index];
const otherNodes = data.filter((otherNode) => otherNode !== node);
if (otherNodes.some(otherNode => otherNode.canInsertAsDescendant(node))) {
data.splice(index, 1);
}
}
count++;
if (count > _generationLimit) {
throw new Error(`Data contains multiple roots or spans more than ${_generationLimit} generations.`);
}
}
return data;
}
export function seed(data: Member[], targetId: number, options?: SeederOptions): TreeNode[] {
const members = _getRelatives(data, targetId);
const marriages = _combineIntoMarriages(members, options);
const rootNode = _coalesce(marriages);
return rootNode;
}
export const _private = {
_getRelatives,
_combineIntoMarriages,
_coalesce
};
}