340 lines
9.1 KiB
TypeScript
340 lines
9.1 KiB
TypeScript
import { mdiXml } from "@mdi/js";
|
|
import Icon from "@mdi/react";
|
|
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
|
import { IconButton, Tooltip } from "@mui/material";
|
|
import "family-chart";
|
|
import { jsPDF } from "jspdf";
|
|
import React from "react";
|
|
import "svg2pdf.js";
|
|
import { Couple } from "../../api/CoupleApi";
|
|
import { Member, fmtDate } from "../../api/MemberApi";
|
|
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
|
|
import {
|
|
FamilyTreeNode,
|
|
getAvailableMembers,
|
|
treeHeight,
|
|
treeWidth,
|
|
} from "../../utils/family_tree";
|
|
import { downloadBlob } from "../../utils/files_utils";
|
|
import "./family-chart.css";
|
|
|
|
export function ComplexFamilyTree(p: {
|
|
tree: FamilyTreeNode;
|
|
isUp: boolean;
|
|
depth: number;
|
|
}): React.ReactElement {
|
|
const darkTheme = useDarkTheme();
|
|
console.log(f3);
|
|
const applyTree = (container: HTMLDivElement) => {
|
|
if (!container) return;
|
|
|
|
const store = f3.createStore({
|
|
data: treeToF3Data(p.tree, p.isUp, p.depth),
|
|
node_separation: 250,
|
|
level_separation: 150,
|
|
});
|
|
const view = f3.d3AnimationView({
|
|
store,
|
|
cont: container,
|
|
});
|
|
const Card = f3.elements.Card({
|
|
store,
|
|
svg: view.svg,
|
|
card_dim: {
|
|
w: 210,
|
|
h: 120,
|
|
text_x: 5,
|
|
text_y: 75,
|
|
img_w: 60,
|
|
img_h: 70,
|
|
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 += `</tspan> <tspan x="0" dy="14" font-size="10">${weddingInfo.join(
|
|
" - "
|
|
)}`;
|
|
}
|
|
|
|
return s;
|
|
},
|
|
],
|
|
mini_tree: true,
|
|
link_break: false,
|
|
});
|
|
|
|
// Patch generated card
|
|
const PatchedCard: f3.F3CardBuilder = (p) => {
|
|
const res = Card(p);
|
|
|
|
// Patch card colors for PDF export
|
|
res
|
|
.querySelector(".card-male")
|
|
?.querySelector(".card-body-rect")
|
|
?.setAttribute("fill", "#add8e6");
|
|
res
|
|
.querySelector(".card-female")
|
|
?.querySelector(".card-body-rect")
|
|
?.setAttribute("fill", "#ffb6c1");
|
|
|
|
return res;
|
|
};
|
|
|
|
view.setCard(PatchedCard);
|
|
store.setOnUpdate((props) => view.update(props || {}));
|
|
store.update.tree({ initial: false, transition_time: 0 });
|
|
};
|
|
|
|
const doExport = async (onlySVG: boolean) => {
|
|
const docWidth = treeWidth(p.tree) * 65;
|
|
const docHeight = treeHeight(p.tree) * 60;
|
|
console.info(`Tree w=${treeWidth(p.tree)} h=${treeHeight(p.tree)}`);
|
|
|
|
// 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(
|
|
`<path class="link" fill="none" stroke="#fff"`,
|
|
`<path class="link" fill="none" stroke="#000"`
|
|
);
|
|
|
|
dstSVG = dstSVG.replaceAll(
|
|
`class="text-overflow-mask"`,
|
|
`class="text-overflow-mask" fill="transparent"`
|
|
);
|
|
|
|
dstSVG = dstSVG.replaceAll(`>UNKNOWN<`, `fill="#000">INCONNU<`);
|
|
|
|
dstSVG = dstSVG.replaceAll(
|
|
`class="card-outline`,
|
|
`fill="transparent" class="card-outline`
|
|
);
|
|
|
|
dstSVG = dstSVG.replaceAll("✝", " ");
|
|
|
|
// Download in SVG format
|
|
if (onlySVG) {
|
|
// Fix background color (first rect background)
|
|
dstSVG = dstSVG.replace(
|
|
`fill="transparent"></rect>`,
|
|
`fill="white"></rect>`
|
|
);
|
|
|
|
const blob = new Blob([`<svg>${dstSVG}</svg>`], {
|
|
type: "image/svg+xml",
|
|
});
|
|
|
|
downloadBlob(blob, "ArbreGenealogique.svg");
|
|
|
|
return;
|
|
}
|
|
|
|
// Download in PDF format
|
|
//navigator.clipboard.writeText(dstSVG);
|
|
target.innerHTML = dstSVG;
|
|
|
|
const doc = new jsPDF({
|
|
orientation: "l",
|
|
format: [docHeight, docWidth],
|
|
});
|
|
|
|
await doc.svg(target, {
|
|
height: docHeight,
|
|
width: docWidth,
|
|
});
|
|
|
|
container.remove();
|
|
|
|
// Save the created pdf
|
|
doc.save("ArbreGenealogique.pdf");
|
|
};
|
|
|
|
const exportPDF = () => doExport(false);
|
|
|
|
const exportSVG = () => doExport(true);
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ textAlign: "right" }}>
|
|
<Tooltip title="Exporter le graphique au format PDF">
|
|
<IconButton onClick={exportPDF}>
|
|
<PictureAsPdfIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip title="Exporter le graphique au format SVG">
|
|
<IconButton onClick={exportSVG}>
|
|
<Icon path={mdiXml} size={1} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</div>
|
|
<div
|
|
style={{ width: "100%" }}
|
|
className={`f3 ${darkTheme.enabled ? "f3-dark" : "f3-light"}`}
|
|
id="FamilyChart"
|
|
ref={applyTree}
|
|
></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function treeToF3Data(
|
|
node: FamilyTreeNode,
|
|
isUp: boolean,
|
|
depth: number
|
|
): f3.f3Data[] {
|
|
const availableMembers = getAvailableMembers(node, depth);
|
|
|
|
const list: f3.f3Data[] = [];
|
|
if (isUp) treeToF3DataUpRecurse(node, list, availableMembers);
|
|
else treeToF3DataDownRecurse(node, list, availableMembers);
|
|
return list;
|
|
}
|
|
|
|
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: f3.f3Data[],
|
|
availableMembers: Set<number>,
|
|
child?: number,
|
|
spouses?: number[]
|
|
) {
|
|
if (!availableMembers.has(node.member.id)) return;
|
|
|
|
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: f3.f3Data[],
|
|
availableMembers: Set<number>
|
|
) {
|
|
if (!availableMembers.has(node.member.id)) return;
|
|
|
|
// Get all members ids
|
|
let children = node?.down?.map((c) => c.member.id) ?? [];
|
|
node.couples?.map((c) => c.down.forEach((m) => children.push(m.member.id)));
|
|
|
|
children = children.filter((c) => availableMembers.has(c));
|
|
|
|
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
|
|
?.filter((s) => availableMembers.has(s.member.id))
|
|
.map((c) => c.member.id.toString()),
|
|
children: children.map((c) => c.toString()),
|
|
},
|
|
});
|
|
|
|
node?.down?.forEach((e) =>
|
|
treeToF3DataDownRecurse(e, array, availableMembers)
|
|
);
|
|
|
|
if (node.couples) {
|
|
for (const c of node.couples) {
|
|
if (!availableMembers.has(c.member.id)) continue;
|
|
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
|
|
.filter((c) => availableMembers.has(c.member.id))
|
|
.map((c) => c.member.id.toString()),
|
|
},
|
|
});
|
|
|
|
c.down.forEach((e) =>
|
|
treeToF3DataDownRecurse(e, array, availableMembers)
|
|
);
|
|
}
|
|
}
|
|
}
|