10 Commits

Author SHA1 Message Date
abb5ef89c9 Fix issue in deployment script
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-08-26 15:55:07 +02:00
9006d73bf9 Fix file encoding
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2023-08-26 15:38:02 +02:00
881b8c0d60 Add PDF export
Some checks failed
continuous-integration/drone/push Build is failing
2023-08-26 15:25:53 +02:00
784f7ecb6b Add dark theme support 2023-08-26 15:12:02 +02:00
d0013d41bb Update simple tree appearance
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-26 15:05:53 +02:00
966bae979b Remove a little improvement 2023-08-26 15:00:00 +02:00
96ca6fd7af Make simple tree browsable
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-26 14:54:17 +02:00
cbaabb34d5 Improve layout a little
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-26 14:41:33 +02:00
dbb6724f44 Fix spacing issues
Some checks failed
continuous-integration/drone/push Build is failing
2023-08-26 14:37:49 +02:00
28d9b16239 Fix couples spacing 2023-08-26 14:34:11 +02:00
8 changed files with 200 additions and 61 deletions

View File

@@ -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",

View File

@@ -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"

View File

@@ -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 ?? "";

View File

@@ -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

View File

@@ -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;
if (node.spouse) {
levelWidth =
SPOUSE_SPACING +
memberCardWidth(node.member) + memberCardWidth(node.member) +
(node.spouse ? SPOUSE_SPACING + memberCardWidth(node.spouse.member) : 0) + memberCardWidth(node.spouse.member);
CARD_SPACING; } 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>
<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>
<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} /> <NodeArea node={tree} x={0} y={0} />
</svg> </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))}

View File

@@ -0,0 +1,7 @@
.simpletree-dark path {
stroke: white;
}
.simpletree-dark tspan {
fill: white;
}

View File

@@ -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