GeneIT/geneit_app/src/widgets/complex_family_tree/ComplexFamilyTree.tsx

339 lines
9.2 KiB
TypeScript
Raw Normal View History

2023-08-22 14:12:55 +00:00
import React from "react";
2023-08-22 15:17:36 +00:00
import f3, { f3Data } from "family-chart";
2023-08-22 14:12:55 +00:00
import "./family-chart.css";
2023-08-23 08:27:51 +00:00
import { FamilyTreeNode } from "../../utils/family_tree";
2023-08-23 08:58:25 +00:00
import { Member, fmtDate } from "../../api/MemberApi";
2023-08-23 09:25:33 +00:00
import { Couple } from "../../api/CoupleApi";
2023-08-23 12:02:05 +00:00
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
2023-08-23 12:56:50 +00:00
import { IconButton } from "@mui/material";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import { jsPDF } from "jspdf";
import "svg2pdf.js";
2023-08-23 14:07:21 +00:00
import { getAllIndexes } from "../../utils/string_utils";
2023-08-22 14:12:55 +00:00
2023-08-23 08:27:51 +00:00
export function ComplexFamilyTree(p: {
tree: FamilyTreeNode;
2023-08-23 08:58:25 +00:00
isUp: boolean;
2023-08-23 08:27:51 +00:00
}): React.ReactElement {
2023-08-23 12:02:05 +00:00
const darkTheme = useDarkTheme();
2023-08-23 13:18:23 +00:00
const applyTree = (container: HTMLDivElement) => {
if (!container) return;
2023-08-22 14:12:55 +00:00
const store = f3.createStore({
2023-08-23 08:58:25 +00:00
data: treeToF3Data(p.tree, p.isUp),
2023-08-22 14:12:55 +00:00
node_separation: 250,
level_separation: 150,
}),
view = f3.d3AnimationView({
store,
2023-08-23 13:18:23 +00:00
cont: container,
2023-08-22 14:12:55 +00:00
}),
Card = f3.elements.Card({
store,
svg: view.svg,
card_dim: {
2023-08-25 17:06:36 +00:00
w: 160,
h: 118,
text_x: 5,
text_y: 70,
2023-08-22 14:12:55 +00:00
img_w: 60,
img_h: 60,
img_x: 5,
img_y: 5,
},
card_display: [
2023-08-23 09:25:33 +00:00
(d) =>
`${d.data.first_name || ""} ${d.data.last_name || ""} ${
d.data.dead ? "✝" : ""
}`,
(d) => {
2023-08-23 14:19:45 +00:00
let birthDeath = [];
if (d.data.birthday) birthDeath.push(d.data.birthday);
if (d.data.deathday) birthDeath.push(d.data.deathday);
let s = birthDeath.join(" -> ");
2023-08-23 09:25:33 +00:00
if (d.data.wedding_state || d.data.dateOfWedding) {
let weddingInfo = [];
if (d.data.wedding_state) weddingInfo.push(d.data.wedding_state);
if (d.data.dateOfWedding)
weddingInfo.push("Mariage : " + d.data.dateOfWedding);
s += `</tspan> <tspan x="0" dy="14" font-size="10">${weddingInfo.join(
" - "
)}`;
}
return s;
},
2023-08-22 14:12:55 +00:00
],
mini_tree: true,
link_break: false,
});
view.setCard(Card);
store.setOnUpdate((props) => view.update(props || {}));
2023-08-23 13:18:23 +00:00
store.update.tree({ initial: false, transition_time: 0 });
};
2023-08-22 14:12:55 +00:00
2023-08-23 12:56:50 +00:00
const exportPDF = async () => {
2023-08-23 13:49:19 +00:00
const docWidth = treeWidth(p.tree) * 60;
2023-08-23 13:42:07 +00:00
const docHeight = treeHeight(p.tree) * 41;
2023-08-23 12:56:50 +00:00
const doc = new jsPDF({
orientation: "l",
2023-08-23 13:49:19 +00:00
format: [docHeight, docWidth],
2023-08-23 12:56:50 +00:00
});
// Clone the SVG to manipulate it
2023-08-23 13:18:23 +00:00
const container = document.createElement("div");
container.classList.add("f3", "f3-export");
2023-08-23 13:49:19 +00:00
container.style.width = docWidth + "px";
2023-08-23 13:42:07 +00:00
container.style.height = docHeight + "px";
2023-08-23 13:18:23 +00:00
document.body.appendChild(container);
applyTree(container);
const target = container.children[0];
await new Promise((res) => setTimeout(() => res(null), 100));
2023-08-23 12:56:50 +00:00
// SVG manipulations (adaptations to export)
2023-08-23 13:18:23 +00:00
let dstSVG = target.innerHTML.replaceAll(
2023-08-23 12:56:50 +00:00
`<path class="link" fill="none" stroke="#fff"`,
`<path class="link" fill="none" stroke="#000"`
);
dstSVG = dstSVG.replaceAll(
`class="card-body-rect"`,
`class="card-body-rect" fill="white"`
);
dstSVG = dstSVG.replaceAll(
`class="text-overflow-mask"`,
`class="text-overflow-mask" fill="transparent"`
);
2023-08-23 13:32:02 +00:00
dstSVG = dstSVG.replaceAll(`>UNKNOWN<`, `fill="#000">INCONNU<`);
2023-08-23 14:24:38 +00:00
dstSVG = dstSVG.replaceAll(
`class="card-outline`,
`fill="transparent" class="card-outline`
);
2023-08-23 14:19:45 +00:00
dstSVG = dstSVG.replaceAll("✝", " ");
2023-08-23 14:07:21 +00:00
let womanTiles = getAllIndexes(dstSVG, "card-female");
2023-08-25 17:06:36 +00:00
let count = 0;
for (let i of womanTiles) {
// Correct padding due to previous corrections
i += count++ * 2;
2023-08-23 14:07:21 +00:00
dstSVG =
dstSVG.substring(0, i) +
dstSVG.substring(i + 1).replace(`fill="white"`, `fill="#ffb6c1"`);
}
2023-08-25 17:06:36 +00:00
count = 0;
2023-08-23 14:07:21 +00:00
let manTiles = getAllIndexes(dstSVG, "card-male");
2023-08-25 17:06:36 +00:00
for (let i of manTiles) {
// Correct padding due to previous corrections
i += count++ * 2;
2023-08-23 14:07:21 +00:00
dstSVG =
dstSVG.substring(0, i) +
dstSVG.substring(i + 1).replace(`fill="white"`, `fill="#add8e6"`);
}
//navigator.clipboard.writeText(dstSVG);
2023-08-23 13:18:23 +00:00
target.innerHTML = dstSVG;
2023-08-23 12:56:50 +00:00
2023-08-23 13:18:23 +00:00
await doc.svg(target, {
2023-08-23 13:42:07 +00:00
height: docHeight,
2023-08-23 13:49:19 +00:00
width: docWidth,
2023-08-23 12:56:50 +00:00
});
2023-08-23 13:18:23 +00:00
container.remove();
2023-08-23 12:56:50 +00:00
// Save the created pdf
2023-08-23 15:57:28 +00:00
doc.save("ArbreGenealogique.pdf");
2023-08-23 12:56:50 +00:00
};
2023-08-23 12:02:05 +00:00
return (
2023-08-23 12:56:50 +00:00
<div>
<div style={{ textAlign: "right" }}>
<IconButton onClick={exportPDF}>
<PictureAsPdfIcon />
</IconButton>
</div>
<div
className={`f3 ${darkTheme.enabled ? "f3-dark" : "f3-light"}`}
id="FamilyChart"
2023-08-23 13:18:23 +00:00
ref={applyTree}
2023-08-23 12:56:50 +00:00
></div>
</div>
2023-08-23 12:02:05 +00:00
);
}
2023-08-22 14:57:41 +00:00
2023-08-23 13:42:07 +00:00
function treeHeight(node: FamilyTreeNode): number {
let res =
node.down?.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0) ?? 0;
node.couples?.forEach(
(c) =>
(res = Math.max(
res,
c.down.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0)
))
);
return res + 1;
}
2023-08-23 13:49:19 +00:00
function treeWidth(node: FamilyTreeNode): number {
const values = new Array(treeHeight(node)).fill(0);
treeWidthRecurse(node, values, 0);
return Math.max(...values);
}
function treeWidthRecurse(node: FamilyTreeNode, vals: number[], level: number) {
vals[level] += 1 + (node.couples?.length ?? 0) + (node.down ? 1 : 0);
node.down?.forEach((n) => treeWidthRecurse(n, vals, level + 1));
node.couples?.forEach((c) =>
c.down.forEach((n) => treeWidthRecurse(n, vals, level + 1))
);
}
2023-08-23 08:58:25 +00:00
function treeToF3Data(node: FamilyTreeNode, isUp: boolean): f3Data[] {
2023-08-23 08:27:51 +00:00
const availableMembers = new Set<number>();
getAvailableMembers(node, availableMembers);
const list: f3Data[] = [];
2023-08-23 09:25:33 +00:00
if (isUp) treeToF3DataUpRecurse(node, list, availableMembers);
else treeToF3DataDownRecurse(node, list, availableMembers);
2023-08-23 08:27:51 +00:00
return list;
}
function getAvailableMembers(t: FamilyTreeNode, s: Set<number>) {
s.add(t.member.id);
t.couples?.forEach((c) => {
s.add(c.member.id);
c.down.forEach((e) => getAvailableMembers(e, s));
});
t.down?.forEach((e) => getAvailableMembers(e, s));
}
2023-08-23 09:25:33 +00:00
function memberData(m: Member, c?: Couple): f3.f3DataData {
2023-08-23 08:58:25 +00:00
return {
first_name: m.first_name ?? "_",
last_name: m.last_name ?? "_",
gender: m.sex ?? "M",
avatar: m.thumbnailURL ?? undefined,
2023-08-23 09:25:33 +00:00
dead: m.dead,
2023-08-23 08:58:25 +00:00
birthday: m.dateOfBirth ? fmtDate(m.dateOfBirth) : undefined,
2023-08-23 09:25:33 +00:00
deathday: m.dateOfDeath ? fmtDate(m.dateOfDeath) : undefined,
wedding_state: c?.stateFr,
dateOfWedding: c?.dateOfWedding ? fmtDate(c?.dateOfWedding) : undefined,
2023-08-23 08:58:25 +00:00
};
}
function treeToF3DataUpRecurse(
node: FamilyTreeNode,
array: f3Data[],
availableMembers: Set<number>,
child?: number,
spouses?: number[]
) {
array.push({
data: memberData(node.member),
id: node.member.id.toString(),
rels: {
father:
node.member.father && availableMembers.has(node.member.father)
? node.member.father.toString()
: undefined,
mother:
node.member.mother && availableMembers.has(node.member.mother)
? node.member.mother.toString()
: undefined,
spouses: spouses
?.filter((c) => c !== node.member.id)
.map((c) => c.toString()),
children: child ? [child.toString()] : undefined,
},
});
const parentSpouses = node.down?.map((c) => c.member.id);
node.down?.forEach((d) =>
treeToF3DataUpRecurse(
d,
array,
availableMembers,
node.member.id,
parentSpouses
)
);
}
function treeToF3DataDownRecurse(
2023-08-23 08:27:51 +00:00
node: FamilyTreeNode,
array: f3Data[],
availableMembers: Set<number>
) {
// Get all members ids
const children = node?.down?.map((c) => c.member.id.toString()) ?? [];
node.couples?.map((c) =>
c.down.forEach((m) => children.push(m.member.id.toString()))
);
array.push({
2023-08-23 08:58:25 +00:00
data: memberData(node.member),
2023-08-23 08:27:51 +00:00
id: node.member.id.toString(),
2023-08-22 15:17:36 +00:00
rels: {
2023-08-23 08:27:51 +00:00
father:
node.member.father && availableMembers.has(node.member.father)
? node.member.father.toString()
: undefined,
mother:
node.member.mother && availableMembers.has(node.member.mother)
? node.member.mother.toString()
: undefined,
spouses: node.couples?.map((c) => c.member.id.toString()),
children: children,
2023-08-22 15:17:36 +00:00
},
2023-08-23 08:27:51 +00:00
});
2023-08-23 08:58:25 +00:00
node?.down?.forEach((e) =>
treeToF3DataDownRecurse(e, array, availableMembers)
);
2023-08-23 08:27:51 +00:00
if (node.couples) {
for (const c of node.couples) {
array.push({
2023-08-23 09:25:33 +00:00
data: memberData(c.member, c.couple),
2023-08-23 08:27:51 +00:00
id: c.member.id.toString(),
rels: {
father:
c.member.father && availableMembers.has(c.member.father)
? c.member.father.toString()
: undefined,
mother:
c.member.mother && availableMembers.has(c.member.mother)
? c.member.mother.toString()
: undefined,
spouses: [node.member.id.toString()],
children: c.down.map((c) => c.member.id.toString()),
},
});
2023-08-23 08:58:25 +00:00
c.down.forEach((e) =>
treeToF3DataDownRecurse(e, array, availableMembers)
);
2023-08-23 08:27:51 +00:00
}
}
}