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) => {
|
2023-08-22 15:08:58 +00:00
|
|
|
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-23 09:25:33 +00:00
|
|
|
w: 230,
|
2023-08-22 14:12:55 +00:00
|
|
|
h: 70,
|
|
|
|
text_x: 75,
|
|
|
|
text_y: 15,
|
|
|
|
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 15:08:58 +00:00
|
|
|
};
|
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");
|
|
|
|
for (const i of womanTiles) {
|
|
|
|
dstSVG =
|
|
|
|
dstSVG.substring(0, i) +
|
|
|
|
dstSVG.substring(i + 1).replace(`fill="white"`, `fill="#ffb6c1"`);
|
|
|
|
}
|
|
|
|
|
|
|
|
let manTiles = getAllIndexes(dstSVG, "card-male");
|
|
|
|
for (const i of manTiles) {
|
|
|
|
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
|
|
|
|
doc.save("myPDF.pdf");
|
|
|
|
};
|
|
|
|
|
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 15:08:58 +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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|