import React from "react"; import f3, { f3Data } from "family-chart"; import "./family-chart.css"; import { FamilyTreeNode } from "../../utils/family_tree"; import { Member, fmtDate } from "../../api/MemberApi"; import { Couple } from "../../api/CoupleApi"; import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; import { IconButton } from "@mui/material"; import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; import { jsPDF } from "jspdf"; import "svg2pdf.js"; import { getAllIndexes } from "../../utils/string_utils"; export function ComplexFamilyTree(p: { tree: FamilyTreeNode; isUp: boolean; }): React.ReactElement { const darkTheme = useDarkTheme(); const applyTree = (container: HTMLDivElement) => { if (!container) return; const store = f3.createStore({ data: treeToF3Data(p.tree, p.isUp), node_separation: 250, level_separation: 150, }), view = f3.d3AnimationView({ store, cont: container, }), Card = f3.elements.Card({ store, svg: view.svg, card_dim: { w: 210, h: 118, text_x: 5, text_y: 70, img_w: 60, img_h: 60, img_x: 5, img_y: 5, }, card_display: [ (d) => `${d.data.first_name || ""} ${d.data.last_name || ""} ${ d.data.dead ? "✝" : "" }`, (d) => { let birthDeath = []; if (d.data.birthday) birthDeath.push(d.data.birthday); if (d.data.deathday) birthDeath.push(d.data.deathday); let s = birthDeath.join(" -> "); 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 += ` ${weddingInfo.join( " - " )}`; } return s; }, ], mini_tree: true, link_break: false, }); view.setCard(Card); store.setOnUpdate((props) => view.update(props || {})); store.update.tree({ initial: false, transition_time: 0 }); }; const exportPDF = async () => { const docWidth = treeWidth(p.tree) * 65; const docHeight = treeHeight(p.tree) * 60; console.info(`Tree w=${treeWidth(p.tree)} h=${treeHeight(p.tree)}`); const doc = new jsPDF({ orientation: "l", format: [docHeight, docWidth], }); // Clone the SVG to manipulate it const container = document.createElement("div"); container.classList.add("f3", "f3-export"); container.style.width = docWidth + "px"; container.style.height = docHeight + "px"; document.body.appendChild(container); applyTree(container); const target = container.children[0]; await new Promise((res) => setTimeout(() => res(null), 100)); // SVG manipulations (adaptations to export) let dstSVG = target.innerHTML.replaceAll( `UNKNOWN<`, `fill="#000">INCONNU<`); dstSVG = dstSVG.replaceAll( `class="card-outline`, `fill="transparent" class="card-outline` ); dstSVG = dstSVG.replaceAll("✝", " "); let womanTiles = getAllIndexes(dstSVG, "card-female"); let count = 0; for (let i of womanTiles) { // Correct padding due to previous corrections i += count++ * 2; dstSVG = dstSVG.substring(0, i) + dstSVG.substring(i + 1).replace(`fill="white"`, `fill="#ffb6c1"`); } count = 0; let manTiles = getAllIndexes(dstSVG, "card-male"); for (let i of manTiles) { // Correct padding due to previous corrections i += count++ * 2; dstSVG = dstSVG.substring(0, i) + dstSVG.substring(i + 1).replace(`fill="white"`, `fill="#add8e6"`); } //navigator.clipboard.writeText(dstSVG); target.innerHTML = dstSVG; await doc.svg(target, { height: docHeight, width: docWidth, }); container.remove(); // Save the created pdf doc.save("ArbreGenealogique.pdf"); }; return (
); } 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; } 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?.length ?? 0) > 0 ? 1 : 0); node.down?.forEach((n) => treeWidthRecurse(n, vals, level + 1)); node.couples?.forEach((c) => c.down.forEach((n) => treeWidthRecurse(n, vals, level + 1)) ); } function treeToF3Data(node: FamilyTreeNode, isUp: boolean): f3Data[] { const availableMembers = new Set(); getAvailableMembers(node, availableMembers); const list: f3Data[] = []; if (isUp) treeToF3DataUpRecurse(node, list, availableMembers); else treeToF3DataDownRecurse(node, list, availableMembers); return list; } function getAvailableMembers(t: FamilyTreeNode, s: Set) { 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)); } function memberData(m: Member, c?: Couple): f3.f3DataData { return { first_name: m.first_name ?? "_", last_name: m.last_name ?? "_", gender: m.sex ?? "M", avatar: m.thumbnailURL ?? undefined, dead: m.dead, birthday: m.dateOfBirth ? fmtDate(m.dateOfBirth) : undefined, deathday: m.dateOfDeath ? fmtDate(m.dateOfDeath) : undefined, wedding_state: c?.stateFr, dateOfWedding: c?.dateOfWedding ? fmtDate(c?.dateOfWedding) : undefined, }; } function treeToF3DataUpRecurse( node: FamilyTreeNode, array: f3Data[], availableMembers: Set, 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( node: FamilyTreeNode, array: f3Data[], availableMembers: Set ) { // 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({ 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: node.couples?.map((c) => c.member.id.toString()), children: children, }, }); node?.down?.forEach((e) => treeToF3DataDownRecurse(e, array, availableMembers) ); if (node.couples) { for (const c of node.couples) { array.push({ data: memberData(c.member, c.couple), 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()), }, }); c.down.forEach((e) => treeToF3DataDownRecurse(e, array, availableMembers) ); } } }