Compare commits
10 Commits
f135287257
...
abb5ef89c9
| Author | SHA1 | Date | |
|---|---|---|---|
| abb5ef89c9 | |||
| 9006d73bf9 | |||
| 881b8c0d60 | |||
| 784f7ecb6b | |||
| d0013d41bb | |||
| 966bae979b | |||
| 96ca6fd7af | |||
| cbaabb34d5 | |||
| dbb6724f44 | |||
| 28d9b16239 |
14
geneit_app/package-lock.json
generated
14
geneit_app/package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
"react-easy-crop": "^5.0.0",
|
"react-easy-crop": "^5.0.0",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
|
"react-zoom-pan-pinch": "^3.1.0",
|
||||||
"svg2pdf.js": "^2.2.2",
|
"svg2pdf.js": "^2.2.2",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
@@ -13603,6 +13604,19 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-zoom-pan-pinch": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-a3LlP8QPgTikvteCNkZ3X6wIWC0lrg1geP5WkUJyx2MXXAhHQek3r17N1nT/esOiWGuPIECnsd9AGoK8jOeGcg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-dom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"react-easy-crop": "^5.0.0",
|
"react-easy-crop": "^5.0.0",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
|
"react-zoom-pan-pinch": "^3.1.0",
|
||||||
"svg2pdf.js": "^2.2.2",
|
"svg2pdf.js": "^2.2.2",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ export class Member implements MemberDataApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get lastNameUpperCase(): string | undefined {
|
||||||
|
return this.last_name?.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
get fullName(): string {
|
get fullName(): string {
|
||||||
const firstName = this.first_name ?? "";
|
const firstName = this.first_name ?? "";
|
||||||
|
|
||||||
|
|||||||
@@ -154,15 +154,11 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* the tree itself */}
|
{/* the tree itself */}
|
||||||
<Paper style={{ flex: "1" }}>
|
<Paper style={{ flex: "1", display: "flex", flexDirection: "column" }}>
|
||||||
{currTab === CurrTab.BasicTree ? (
|
{currTab === CurrTab.BasicTree ? (
|
||||||
<BasicFamilyTree tree={tree!} depth={currDepth} />
|
<BasicFamilyTree tree={tree!} depth={currDepth} />
|
||||||
) : currTab === CurrTab.SimpleTree ? (
|
) : currTab === CurrTab.SimpleTree ? (
|
||||||
<SimpleFamilyTree
|
<SimpleFamilyTree tree={tree!} depth={currDepth} />
|
||||||
tree={tree!}
|
|
||||||
isUp={currMode === TreeMode.Ascending}
|
|
||||||
depth={currDepth}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<ComplexFamilyTree
|
<ComplexFamilyTree
|
||||||
tree={tree!}
|
tree={tree!}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,17 +1,45 @@
|
|||||||
|
import { mdiXml } from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
import jsPDF from "jspdf";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||||
import { Couple } from "../../api/CoupleApi";
|
import { Couple } from "../../api/CoupleApi";
|
||||||
import { Member } from "../../api/MemberApi";
|
import { Member } from "../../api/MemberApi";
|
||||||
|
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
|
||||||
import { FamilyTreeNode } from "../../utils/family_tree";
|
import { FamilyTreeNode } from "../../utils/family_tree";
|
||||||
|
import { downloadBlob } from "../../utils/files_utils";
|
||||||
import { getTextWidth } from "../../utils/render_utils";
|
import { getTextWidth } from "../../utils/render_utils";
|
||||||
|
import "./simpletree.css";
|
||||||
|
import "./Roboto-normal";
|
||||||
|
|
||||||
const FACE_WIDTH = 60;
|
const FACE_WIDTH = 60;
|
||||||
const FACE_HEIGHT = 70;
|
const FACE_HEIGHT = 70;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertical space between faces and text
|
||||||
|
*/
|
||||||
const FACE_TEXT_SPACING = 2;
|
const FACE_TEXT_SPACING = 2;
|
||||||
|
|
||||||
const CARD_HEIGHT = 103;
|
/**
|
||||||
|
* Cards height
|
||||||
|
*/
|
||||||
|
const CARD_HEIGHT = 114;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Space between spouse cards
|
||||||
|
*/
|
||||||
const SPOUSE_SPACING = 10;
|
const SPOUSE_SPACING = 10;
|
||||||
const CARD_SPACING = 20;
|
|
||||||
const SIBLINGS_SPACING = 20;
|
/**
|
||||||
|
* Space between two siblings hierachy
|
||||||
|
*/
|
||||||
|
const SIBLINGS_SPACING = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertical space between two generations
|
||||||
|
*/
|
||||||
const LEVEL_SPACING = 25;
|
const LEVEL_SPACING = 25;
|
||||||
|
|
||||||
const NAME_FONT = "13px Roboto";
|
const NAME_FONT = "13px Roboto";
|
||||||
@@ -26,13 +54,28 @@ interface SimpleTreeNode {
|
|||||||
member: Member;
|
member: Member;
|
||||||
spouse?: SimpleTreeSpouseInfo;
|
spouse?: SimpleTreeSpouseInfo;
|
||||||
down: SimpleTreeNode[];
|
down: SimpleTreeNode[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width of the parent and its children
|
||||||
|
*/
|
||||||
width: number;
|
width: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width of the parents
|
||||||
|
*/
|
||||||
|
parentWidth: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sum of the width of the children
|
||||||
|
*/
|
||||||
|
childrenWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the width of a member card
|
* Get the width of a member card
|
||||||
*/
|
*/
|
||||||
function memberCardWidth(m: Member): number {
|
function memberCardWidth(m?: Member): number {
|
||||||
|
if (!m) return 0;
|
||||||
const nameWidth = getTextWidth(m.fullName, NAME_FONT);
|
const nameWidth = getTextWidth(m.fullName, NAME_FONT);
|
||||||
const birthDeathWidth = getTextWidth(m.displayBirthDeath, BIRTH_FONT);
|
const birthDeathWidth = getTextWidth(m.displayBirthDeath, BIRTH_FONT);
|
||||||
return Math.max(FACE_WIDTH, nameWidth, birthDeathWidth);
|
return Math.max(FACE_WIDTH, nameWidth, birthDeathWidth);
|
||||||
@@ -42,7 +85,7 @@ function center(container_width: number, el_width: number): number {
|
|||||||
return Math.floor((container_width - el_width) / 2);
|
return Math.floor((container_width - el_width) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSimpleDownTreeNode(
|
function buildSimpleTreeNode(
|
||||||
tree: FamilyTreeNode,
|
tree: FamilyTreeNode,
|
||||||
depth: number
|
depth: number
|
||||||
): SimpleTreeNode {
|
): SimpleTreeNode {
|
||||||
@@ -58,10 +101,9 @@ function buildSimpleDownTreeNode(
|
|||||||
);
|
);
|
||||||
else childrenToProcess = [];
|
else childrenToProcess = [];
|
||||||
|
|
||||||
const node = {
|
const node: SimpleTreeNode = {
|
||||||
down:
|
down:
|
||||||
childrenToProcess?.map((c) => buildSimpleDownTreeNode(c, depth - 1)) ??
|
childrenToProcess?.map((c) => buildSimpleTreeNode(c, depth - 1)) ?? [],
|
||||||
[],
|
|
||||||
member: tree.member,
|
member: tree.member,
|
||||||
spouse: lastCouple
|
spouse: lastCouple
|
||||||
? {
|
? {
|
||||||
@@ -69,41 +111,34 @@ function buildSimpleDownTreeNode(
|
|||||||
member: lastCouple.member,
|
member: lastCouple.member,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
parentWidth: -1,
|
||||||
|
childrenWidth: -1,
|
||||||
width: -1,
|
width: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compute current level width
|
// Compute current level width
|
||||||
let levelWidth =
|
let levelWidth: number;
|
||||||
memberCardWidth(node.member) +
|
if (node.spouse) {
|
||||||
(node.spouse ? SPOUSE_SPACING + memberCardWidth(node.spouse.member) : 0) +
|
levelWidth =
|
||||||
CARD_SPACING;
|
SPOUSE_SPACING +
|
||||||
|
memberCardWidth(node.member) +
|
||||||
|
memberCardWidth(node.spouse.member);
|
||||||
|
} else {
|
||||||
|
levelWidth = memberCardWidth(node.member);
|
||||||
|
}
|
||||||
|
|
||||||
// Compute down level width
|
// Compute down level width
|
||||||
const downWidth =
|
const downWidth =
|
||||||
SIBLINGS_SPACING * node.down.length +
|
SIBLINGS_SPACING * node.down.length -
|
||||||
|
SIBLINGS_SPACING +
|
||||||
node.down.reduce((prev, n) => prev + n.width, 0);
|
node.down.reduce((prev, n) => prev + n.width, 0);
|
||||||
|
|
||||||
|
node.parentWidth = levelWidth;
|
||||||
|
node.childrenWidth = downWidth;
|
||||||
node.width = Math.max(levelWidth, downWidth);
|
node.width = Math.max(levelWidth, downWidth);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO : if useless, remove and merge up and down techniques
|
|
||||||
function buildSimpleUpTreeNode(
|
|
||||||
tree: FamilyTreeNode,
|
|
||||||
depth: number
|
|
||||||
): SimpleTreeNode {
|
|
||||||
/*if (depth === 0) throw new Error("Too much recursion reached!");
|
|
||||||
|
|
||||||
return {
|
|
||||||
member: tree.member,
|
|
||||||
children:
|
|
||||||
depth === 1
|
|
||||||
? []
|
|
||||||
: tree.down?.map((c) => buildSimpleUpTreeNode(c, depth - 1)) ?? [],
|
|
||||||
};*/
|
|
||||||
return buildSimpleDownTreeNode(tree, depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple family tree
|
* Simple family tree
|
||||||
*
|
*
|
||||||
@@ -112,25 +147,88 @@ function buildSimpleUpTreeNode(
|
|||||||
|
|
||||||
export function SimpleFamilyTree(p: {
|
export function SimpleFamilyTree(p: {
|
||||||
tree: FamilyTreeNode;
|
tree: FamilyTreeNode;
|
||||||
isUp: boolean;
|
|
||||||
depth: number;
|
depth: number;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
|
const darkTheme = useDarkTheme();
|
||||||
|
|
||||||
const tree = React.useMemo(
|
const tree = React.useMemo(
|
||||||
() =>
|
() => buildSimpleTreeNode(p.tree, p.depth),
|
||||||
p.isUp
|
[p.tree, p.depth]
|
||||||
? buildSimpleUpTreeNode(p.tree, p.depth)
|
|
||||||
: buildSimpleDownTreeNode(p.tree, p.depth),
|
|
||||||
[p.tree, p.isUp, p.depth]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const height = p.depth * (CARD_HEIGHT + LEVEL_SPACING) - LEVEL_SPACING;
|
const height = p.depth * (CARD_HEIGHT + LEVEL_SPACING) - LEVEL_SPACING;
|
||||||
|
|
||||||
console.info(`tree width=${tree.width} height=${height}`);
|
console.info(`tree width=${tree.width} height=${height}`);
|
||||||
|
|
||||||
|
const doExport = async (onlySVG: boolean) => {
|
||||||
|
const el = document.querySelector(".simpletree")!;
|
||||||
|
const svg = el.outerHTML;
|
||||||
|
|
||||||
|
// Download in SVG format
|
||||||
|
if (onlySVG) {
|
||||||
|
const blob = new Blob([svg], {
|
||||||
|
type: "image/svg+xml",
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadBlob(blob, "ArbreGenealogique.svg");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PDF_MARGIN = 10;
|
||||||
|
|
||||||
|
// Download in PDF format
|
||||||
|
const doc = new jsPDF({
|
||||||
|
orientation: "l",
|
||||||
|
format: [height + PDF_MARGIN * 2, tree.width + PDF_MARGIN * 2],
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.setFont("Roboto", "normal");
|
||||||
|
|
||||||
|
await doc.svg(el, {
|
||||||
|
x: PDF_MARGIN,
|
||||||
|
y: PDF_MARGIN,
|
||||||
|
height: height,
|
||||||
|
width: tree.width,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the created pdf
|
||||||
|
doc.save("ArbreGenealogique.pdf");
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportPDF = () => doExport(false);
|
||||||
|
|
||||||
|
const exportSVG = () => doExport(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg width={tree.width} height={height} xmlns="http://www.w3.org/2000/svg">
|
<div>
|
||||||
<NodeArea node={tree} x={0} y={0} />
|
<div style={{ textAlign: "right" }}>
|
||||||
</svg>
|
<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>
|
||||||
|
<TransformWrapper maxScale={15} minScale={0.2}>
|
||||||
|
<TransformComponent wrapperStyle={{ width: "100%", flex: "1" }}>
|
||||||
|
<svg
|
||||||
|
className={`simpletree ${
|
||||||
|
darkTheme.enabled ? "simpletree-dark" : ""
|
||||||
|
}`}
|
||||||
|
width={tree.width}
|
||||||
|
height={height}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<NodeArea node={tree} x={0} y={0} />
|
||||||
|
</svg>
|
||||||
|
</TransformComponent>
|
||||||
|
</TransformWrapper>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,18 +239,12 @@ function NodeArea(p: {
|
|||||||
childrenLinkDestY?: number;
|
childrenLinkDestY?: number;
|
||||||
node: SimpleTreeNode;
|
node: SimpleTreeNode;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const parent_x_offset =
|
let parent_x_offset: number;
|
||||||
p.x +
|
|
||||||
Math.floor(
|
|
||||||
(p.node.width -
|
|
||||||
(memberCardWidth(p.node.member) +
|
|
||||||
(p.node.spouse
|
|
||||||
? SPOUSE_SPACING + memberCardWidth(p.node.spouse.member)
|
|
||||||
: 0))) /
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
let downXOffset = p.x;
|
parent_x_offset = p.x + center(p.node.width, p.node.parentWidth);
|
||||||
|
|
||||||
|
let unusedChildrenWidth = p.node.width - p.node.childrenWidth;
|
||||||
|
let downXOffset = p.x + Math.floor(unusedChildrenWidth / 2);
|
||||||
|
|
||||||
let endFirstFaceX =
|
let endFirstFaceX =
|
||||||
parent_x_offset +
|
parent_x_offset +
|
||||||
@@ -160,7 +252,9 @@ function NodeArea(p: {
|
|||||||
FACE_WIDTH;
|
FACE_WIDTH;
|
||||||
|
|
||||||
let beginingOfSecondCardX =
|
let beginingOfSecondCardX =
|
||||||
parent_x_offset + memberCardWidth(p.node.member) + SPOUSE_SPACING;
|
parent_x_offset +
|
||||||
|
p.node.parentWidth -
|
||||||
|
memberCardWidth(p.node.spouse?.member);
|
||||||
|
|
||||||
let beginSecondFaceX =
|
let beginSecondFaceX =
|
||||||
p.node.spouse &&
|
p.node.spouse &&
|
||||||
@@ -174,6 +268,10 @@ function NodeArea(p: {
|
|||||||
parent_x_offset + Math.floor(memberCardWidth(p.node.member) / 2);
|
parent_x_offset + Math.floor(memberCardWidth(p.node.member) / 2);
|
||||||
let parentLinkY = p.y;
|
let parentLinkY = p.y;
|
||||||
|
|
||||||
|
// Remove ugly little shifts
|
||||||
|
if (Math.abs(parentLinkX - (p.childrenLinkDestX ?? 0)) < 10)
|
||||||
|
parentLinkX = p.childrenLinkDestX!;
|
||||||
|
|
||||||
let childrenLinkX: number;
|
let childrenLinkX: number;
|
||||||
let childrenLinkY: number;
|
let childrenLinkY: number;
|
||||||
|
|
||||||
@@ -183,7 +281,7 @@ function NodeArea(p: {
|
|||||||
} else {
|
} else {
|
||||||
childrenLinkX =
|
childrenLinkX =
|
||||||
parent_x_offset + Math.floor(memberCardWidth(p.node.member) / 2);
|
parent_x_offset + Math.floor(memberCardWidth(p.node.member) / 2);
|
||||||
childrenLinkY = p.y + CARD_HEIGHT;
|
childrenLinkY = p.y + CARD_HEIGHT + 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -268,12 +366,23 @@ function MemberCard(p: {
|
|||||||
{/* Member text */}
|
{/* Member text */}
|
||||||
<text y={FACE_HEIGHT + FACE_TEXT_SPACING}>
|
<text y={FACE_HEIGHT + FACE_TEXT_SPACING}>
|
||||||
<tspan
|
<tspan
|
||||||
x={center(w, getTextWidth(p.member.fullName, NAME_FONT))}
|
x={center(w, getTextWidth(p.member.first_name ?? "", NAME_FONT))}
|
||||||
dy="14"
|
dy="14"
|
||||||
font-size="13"
|
font-size="13"
|
||||||
fontFamily="Roboto"
|
fontFamily="Roboto"
|
||||||
>
|
>
|
||||||
{p.member.fullName}
|
{p.member.first_name ?? ""}
|
||||||
|
</tspan>
|
||||||
|
<tspan
|
||||||
|
x={center(
|
||||||
|
w,
|
||||||
|
getTextWidth(p.member.lastNameUpperCase ?? "", NAME_FONT)
|
||||||
|
)}
|
||||||
|
dy="14"
|
||||||
|
font-size="13"
|
||||||
|
fontFamily="Roboto"
|
||||||
|
>
|
||||||
|
{p.member.lastNameUpperCase ?? ""}
|
||||||
</tspan>
|
</tspan>
|
||||||
<tspan
|
<tspan
|
||||||
x={center(w, getTextWidth(p.member.displayBirthDeath, BIRTH_FONT))}
|
x={center(w, getTextWidth(p.member.displayBirthDeath, BIRTH_FONT))}
|
||||||
|
|||||||
7
geneit_app/src/widgets/simple_family_tree/simpletree.css
Normal file
7
geneit_app/src/widgets/simple_family_tree/simpletree.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.simpletree-dark path {
|
||||||
|
stroke: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simpletree-dark tspan {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ if [[ -z "${AWS_SECRET_ACCESS_KEY}" ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! "$DRONE_COMMIT_BRANCH" == "master"]; then
|
if [ ! "$DRONE_COMMIT_BRANCH" == "master" ]; then
|
||||||
echo Not on master branch. Skip deployment.
|
echo Not on master branch. Skip deployment.
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user