Add simple tree graph mode #4
							
								
								
									
										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,12 +107,16 @@ 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 ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return firstName.length === 0
 | 
					    return firstName.length === 0
 | 
				
			||||||
      ? this.last_name ?? ""
 | 
					      ? this.last_name ?? ""
 | 
				
			||||||
      : `${firstName} ${this.last_name ?? ""}`;
 | 
					      : `${firstName} ${this.last_name?.toUpperCase() ?? ""}`;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get invertedFullName(): string {
 | 
					  get invertedFullName(): string {
 | 
				
			||||||
@@ -159,6 +163,13 @@ export class Member implements MemberDataApi {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get displayBirthDeath(): string {
 | 
				
			||||||
 | 
					    let birthDeath = [];
 | 
				
			||||||
 | 
					    if (this.dateOfBirth) birthDeath.push(fmtDate(this.dateOfBirth));
 | 
				
			||||||
 | 
					    if (this.dateOfDeath) birthDeath.push(fmtDate(this.dateOfDeath));
 | 
				
			||||||
 | 
					    return birthDeath.join(" - ");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get hasContactInfo(): boolean {
 | 
					  get hasContactInfo(): boolean {
 | 
				
			||||||
    return this.email ||
 | 
					    return this.email ||
 | 
				
			||||||
      this.phone ||
 | 
					      this.phone ||
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,9 +27,11 @@ import { BasicFamilyTree } from "../../widgets/BasicFamilyTree";
 | 
				
			|||||||
import { MemberItem } from "../../widgets/MemberItem";
 | 
					import { MemberItem } from "../../widgets/MemberItem";
 | 
				
			||||||
import { RouterLink } from "../../widgets/RouterLink";
 | 
					import { RouterLink } from "../../widgets/RouterLink";
 | 
				
			||||||
import { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree";
 | 
					import { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree";
 | 
				
			||||||
 | 
					import { SimpleFamilyTree } from "../../widgets/simple_family_tree/SimpleFamilyTree";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum CurrTab {
 | 
					enum CurrTab {
 | 
				
			||||||
  BasicTree,
 | 
					  BasicTree,
 | 
				
			||||||
 | 
					  SimpleTree,
 | 
				
			||||||
  AdvancedTree,
 | 
					  AdvancedTree,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,7 +45,7 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const family = useFamily();
 | 
					  const family = useFamily();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [currTab, setCurrTab] = React.useState(CurrTab.AdvancedTree);
 | 
					  const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree);
 | 
				
			||||||
  const [currMode, setCurrMode] = React.useState(TreeMode.Descending);
 | 
					  const [currMode, setCurrMode] = React.useState(TreeMode.Descending);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const member = family.members.get(Number(memberId));
 | 
					  const member = family.members.get(Number(memberId));
 | 
				
			||||||
@@ -146,14 +148,17 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
 | 
				
			|||||||
          aria-label="basic tabs example"
 | 
					          aria-label="basic tabs example"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Tab tabIndex={CurrTab.BasicTree} label="Basique" />
 | 
					          <Tab tabIndex={CurrTab.BasicTree} label="Basique" />
 | 
				
			||||||
 | 
					          <Tab tabIndex={CurrTab.SimpleTree} label="Simple" />
 | 
				
			||||||
          <Tab tabIndex={CurrTab.AdvancedTree} label="Avancé" />
 | 
					          <Tab tabIndex={CurrTab.AdvancedTree} label="Avancé" />
 | 
				
			||||||
        </Tabs>
 | 
					        </Tabs>
 | 
				
			||||||
      </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 ? (
 | 
				
			||||||
 | 
					          <SimpleFamilyTree tree={tree!} depth={currDepth} />
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
          <ComplexFamilyTree
 | 
					          <ComplexFamilyTree
 | 
				
			||||||
            tree={tree!}
 | 
					            tree={tree!}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								geneit_app/src/utils/render_utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								geneit_app/src/utils/render_utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					let canvas: HTMLCanvasElement = document.createElement("canvas");
 | 
				
			||||||
 | 
					let charLen: Map<string, Map<string, number>> = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {String} text The text to be rendered.
 | 
				
			||||||
 | 
					 * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function computeTextWidth(text: string, font: string) {
 | 
				
			||||||
 | 
					  // re-use canvas object for better performance
 | 
				
			||||||
 | 
					  const context = canvas.getContext("2d")!;
 | 
				
			||||||
 | 
					  context.font = font;
 | 
				
			||||||
 | 
					  const metrics = context.measureText(text);
 | 
				
			||||||
 | 
					  return metrics.width;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getTextWidth(text: string, font: string) {
 | 
				
			||||||
 | 
					  if (!charLen.has(font)) charLen.set(font, new Map());
 | 
				
			||||||
 | 
					  const ref = charLen.get(font)!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let size = 0;
 | 
				
			||||||
 | 
					  for (const c of text) {
 | 
				
			||||||
 | 
					    if (!ref.has(c)) ref.set(c, computeTextWidth(c, font));
 | 
				
			||||||
 | 
					    size += ref.get(c)!;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return size;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										419
									
								
								geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										419
									
								
								geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,419 @@
 | 
				
			|||||||
 | 
					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 { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
 | 
				
			||||||
 | 
					import { Couple } from "../../api/CoupleApi";
 | 
				
			||||||
 | 
					import { Member } from "../../api/MemberApi";
 | 
				
			||||||
 | 
					import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
 | 
				
			||||||
 | 
					import { FamilyTreeNode } from "../../utils/family_tree";
 | 
				
			||||||
 | 
					import { downloadBlob } from "../../utils/files_utils";
 | 
				
			||||||
 | 
					import { getTextWidth } from "../../utils/render_utils";
 | 
				
			||||||
 | 
					import "./simpletree.css";
 | 
				
			||||||
 | 
					import "./Roboto-normal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FACE_WIDTH = 60;
 | 
				
			||||||
 | 
					const FACE_HEIGHT = 70;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Vertical space between faces and text
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const FACE_TEXT_SPACING = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Cards height
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const CARD_HEIGHT = 114;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Space between spouse cards
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const SPOUSE_SPACING = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Space between two siblings hierachy
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const SIBLINGS_SPACING = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Vertical space between two generations
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const LEVEL_SPACING = 25;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NAME_FONT = "13px Roboto";
 | 
				
			||||||
 | 
					const BIRTH_FONT = "10px Roboto";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SimpleTreeSpouseInfo {
 | 
				
			||||||
 | 
					  member: Member;
 | 
				
			||||||
 | 
					  couple: Couple;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SimpleTreeNode {
 | 
				
			||||||
 | 
					  member: Member;
 | 
				
			||||||
 | 
					  spouse?: SimpleTreeSpouseInfo;
 | 
				
			||||||
 | 
					  down: SimpleTreeNode[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * The width of the parent and its children
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function memberCardWidth(m?: Member): number {
 | 
				
			||||||
 | 
					  if (!m) return 0;
 | 
				
			||||||
 | 
					  const nameWidth = getTextWidth(m.fullName, NAME_FONT);
 | 
				
			||||||
 | 
					  const birthDeathWidth = getTextWidth(m.displayBirthDeath, BIRTH_FONT);
 | 
				
			||||||
 | 
					  return Math.max(FACE_WIDTH, nameWidth, birthDeathWidth);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function center(container_width: number, el_width: number): number {
 | 
				
			||||||
 | 
					  return Math.floor((container_width - el_width) / 2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function buildSimpleTreeNode(
 | 
				
			||||||
 | 
					  tree: FamilyTreeNode,
 | 
				
			||||||
 | 
					  depth: number
 | 
				
			||||||
 | 
					): SimpleTreeNode {
 | 
				
			||||||
 | 
					  if (depth === 0) throw new Error("Too much recursion reached!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const lastCouple = tree.couples?.[tree.couples?.length - 1 ?? 0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Preprocess children
 | 
				
			||||||
 | 
					  let childrenToProcess = tree.down;
 | 
				
			||||||
 | 
					  if (depth > 1)
 | 
				
			||||||
 | 
					    tree.couples?.forEach(
 | 
				
			||||||
 | 
					      (c) => (childrenToProcess = childrenToProcess?.concat(c.down))
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  else childrenToProcess = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const node: SimpleTreeNode = {
 | 
				
			||||||
 | 
					    down:
 | 
				
			||||||
 | 
					      childrenToProcess?.map((c) => buildSimpleTreeNode(c, depth - 1)) ?? [],
 | 
				
			||||||
 | 
					    member: tree.member,
 | 
				
			||||||
 | 
					    spouse: lastCouple
 | 
				
			||||||
 | 
					      ? {
 | 
				
			||||||
 | 
					          couple: lastCouple.couple,
 | 
				
			||||||
 | 
					          member: lastCouple.member,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      : undefined,
 | 
				
			||||||
 | 
					    parentWidth: -1,
 | 
				
			||||||
 | 
					    childrenWidth: -1,
 | 
				
			||||||
 | 
					    width: -1,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Compute current level width
 | 
				
			||||||
 | 
					  let levelWidth: number;
 | 
				
			||||||
 | 
					  if (node.spouse) {
 | 
				
			||||||
 | 
					    levelWidth =
 | 
				
			||||||
 | 
					      SPOUSE_SPACING +
 | 
				
			||||||
 | 
					      memberCardWidth(node.member) +
 | 
				
			||||||
 | 
					      memberCardWidth(node.spouse.member);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    levelWidth = memberCardWidth(node.member);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Compute down level width
 | 
				
			||||||
 | 
					  const downWidth =
 | 
				
			||||||
 | 
					    SIBLINGS_SPACING * node.down.length -
 | 
				
			||||||
 | 
					    SIBLINGS_SPACING +
 | 
				
			||||||
 | 
					    node.down.reduce((prev, n) => prev + n.width, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  node.parentWidth = levelWidth;
 | 
				
			||||||
 | 
					  node.childrenWidth = downWidth;
 | 
				
			||||||
 | 
					  node.width = Math.max(levelWidth, downWidth);
 | 
				
			||||||
 | 
					  return node;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Simple family tree
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Only one couple can be shown in this version
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SimpleFamilyTree(p: {
 | 
				
			||||||
 | 
					  tree: FamilyTreeNode;
 | 
				
			||||||
 | 
					  depth: number;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const darkTheme = useDarkTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const tree = React.useMemo(
 | 
				
			||||||
 | 
					    () => buildSimpleTreeNode(p.tree, p.depth),
 | 
				
			||||||
 | 
					    [p.tree, p.depth]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const height = p.depth * (CARD_HEIGHT + LEVEL_SPACING) - LEVEL_SPACING;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 (
 | 
				
			||||||
 | 
					    <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} />
 | 
				
			||||||
 | 
					          </svg>
 | 
				
			||||||
 | 
					        </TransformComponent>
 | 
				
			||||||
 | 
					      </TransformWrapper>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function NodeArea(p: {
 | 
				
			||||||
 | 
					  x: number;
 | 
				
			||||||
 | 
					  y: number;
 | 
				
			||||||
 | 
					  childrenLinkDestX?: number;
 | 
				
			||||||
 | 
					  childrenLinkDestY?: number;
 | 
				
			||||||
 | 
					  node: SimpleTreeNode;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  let parent_x_offset: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 =
 | 
				
			||||||
 | 
					    parent_x_offset +
 | 
				
			||||||
 | 
					    Math.floor((memberCardWidth(p.node.member) - FACE_WIDTH) / 2) +
 | 
				
			||||||
 | 
					    FACE_WIDTH;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let beginingOfSecondCardX =
 | 
				
			||||||
 | 
					    parent_x_offset +
 | 
				
			||||||
 | 
					    p.node.parentWidth -
 | 
				
			||||||
 | 
					    memberCardWidth(p.node.spouse?.member);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let beginSecondFaceX =
 | 
				
			||||||
 | 
					    p.node.spouse &&
 | 
				
			||||||
 | 
					    beginingOfSecondCardX +
 | 
				
			||||||
 | 
					      (memberCardWidth(p.node.spouse.member) - FACE_WIDTH) / 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let middleParentFaceY = p.y + Math.floor(FACE_HEIGHT / 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Compute points for link between children and parent
 | 
				
			||||||
 | 
					  let parentLinkX =
 | 
				
			||||||
 | 
					    parent_x_offset + Math.floor(memberCardWidth(p.node.member) / 2);
 | 
				
			||||||
 | 
					  let parentLinkY = p.y;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Remove ugly little shifts
 | 
				
			||||||
 | 
					  if (Math.abs(parentLinkX - (p.childrenLinkDestX ?? 0)) < 10)
 | 
				
			||||||
 | 
					    parentLinkX = p.childrenLinkDestX!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let childrenLinkX: number;
 | 
				
			||||||
 | 
					  let childrenLinkY: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (p.node.spouse) {
 | 
				
			||||||
 | 
					    childrenLinkX = Math.floor((endFirstFaceX + beginSecondFaceX!) / 2);
 | 
				
			||||||
 | 
					    childrenLinkY = middleParentFaceY;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    childrenLinkX =
 | 
				
			||||||
 | 
					      parent_x_offset + Math.floor(memberCardWidth(p.node.member) / 2);
 | 
				
			||||||
 | 
					    childrenLinkY = p.y + CARD_HEIGHT + 2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      {/* Parent link */}
 | 
				
			||||||
 | 
					      {p.childrenLinkDestX && (
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					          className="link"
 | 
				
			||||||
 | 
					          fill="none"
 | 
				
			||||||
 | 
					          stroke="#000"
 | 
				
			||||||
 | 
					          d={`M${p.childrenLinkDestX} ${p.childrenLinkDestY} V ${
 | 
				
			||||||
 | 
					            parentLinkY - Math.floor(LEVEL_SPACING / 2)
 | 
				
			||||||
 | 
					          } H${parentLinkX} V${parentLinkY}`}
 | 
				
			||||||
 | 
					        ></path>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <MemberCard x={parent_x_offset} y={p.y} member={p.node.member} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {p.node.spouse && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          {/* Couple link */}
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            className="link"
 | 
				
			||||||
 | 
					            fill="none"
 | 
				
			||||||
 | 
					            stroke="#000"
 | 
				
			||||||
 | 
					            d={`M${endFirstFaceX} ${middleParentFaceY} H ${beginSecondFaceX}`}
 | 
				
			||||||
 | 
					          ></path>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <MemberCard
 | 
				
			||||||
 | 
					            x={beginingOfSecondCardX}
 | 
				
			||||||
 | 
					            y={p.y}
 | 
				
			||||||
 | 
					            member={p.node.spouse.member}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {p.node.down.map((n) => {
 | 
				
			||||||
 | 
					        const el = (
 | 
				
			||||||
 | 
					          <NodeArea
 | 
				
			||||||
 | 
					            x={downXOffset}
 | 
				
			||||||
 | 
					            y={p.y + CARD_HEIGHT + LEVEL_SPACING}
 | 
				
			||||||
 | 
					            childrenLinkDestX={childrenLinkX}
 | 
				
			||||||
 | 
					            childrenLinkDestY={childrenLinkY}
 | 
				
			||||||
 | 
					            node={n}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        downXOffset += n.width + SIBLINGS_SPACING;
 | 
				
			||||||
 | 
					        return el;
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/*<circle cx={childrenLinkX} cy={childrenLinkY} r="2" fill="red" />
 | 
				
			||||||
 | 
					      <circle cx={parentLinkX} cy={parentLinkY} r="2" fill="green" />*/}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function MemberCard(p: {
 | 
				
			||||||
 | 
					  x: number;
 | 
				
			||||||
 | 
					  y: number;
 | 
				
			||||||
 | 
					  member: Member;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const w = memberCardWidth(p.member);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <g transform={`translate(${p.x} ${p.y})`} width={w} height={CARD_HEIGHT}>
 | 
				
			||||||
 | 
					      {/* Member image */}
 | 
				
			||||||
 | 
					      {p.member.hasPhoto ? (
 | 
				
			||||||
 | 
					        <image
 | 
				
			||||||
 | 
					          x={center(w, FACE_WIDTH)}
 | 
				
			||||||
 | 
					          href={p.member.thumbnailURL!}
 | 
				
			||||||
 | 
					          height={FACE_HEIGHT}
 | 
				
			||||||
 | 
					          width={FACE_WIDTH}
 | 
				
			||||||
 | 
					          preserveAspectRatio="xMidYMin slice"
 | 
				
			||||||
 | 
					        ></image>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <GenderlessIcon
 | 
				
			||||||
 | 
					          x={center(w, FACE_WIDTH)}
 | 
				
			||||||
 | 
					          width={FACE_WIDTH}
 | 
				
			||||||
 | 
					          height={FACE_HEIGHT}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* Member text */}
 | 
				
			||||||
 | 
					      <text y={FACE_HEIGHT + FACE_TEXT_SPACING}>
 | 
				
			||||||
 | 
					        <tspan
 | 
				
			||||||
 | 
					          x={center(w, getTextWidth(p.member.first_name ?? "", NAME_FONT))}
 | 
				
			||||||
 | 
					          dy="14"
 | 
				
			||||||
 | 
					          font-size="13"
 | 
				
			||||||
 | 
					          fontFamily="Roboto"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {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
 | 
				
			||||||
 | 
					          x={center(w, getTextWidth(p.member.displayBirthDeath, BIRTH_FONT))}
 | 
				
			||||||
 | 
					          dy="14"
 | 
				
			||||||
 | 
					          font-size="10"
 | 
				
			||||||
 | 
					          fontFamily="Roboto"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {p.member.displayBirthDeath}
 | 
				
			||||||
 | 
					        </tspan>
 | 
				
			||||||
 | 
					      </text>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function GenderlessIcon(p: {
 | 
				
			||||||
 | 
					  x?: number;
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <g transform={`translate(${p.x ?? 0} 0)`}>
 | 
				
			||||||
 | 
					      <rect height={p.height} width={p.width} fill="rgb(59, 85, 96)" />
 | 
				
			||||||
 | 
					      <g transform={`scale(${p.width * 0.001616})`}>
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					          transform="translate(50,40)"
 | 
				
			||||||
 | 
					          fill="lightgrey"
 | 
				
			||||||
 | 
					          d="M256 288c79.5 0 144-64.5 144-144S335.5 0 256 0 112 
 | 
				
			||||||
 | 
					          64.5 112 144s64.5 144 144 144zm128 32h-55.1c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16H128C57.3 320 0 377.3 
 | 
				
			||||||
 | 
					          0 448v16c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48v-16c0-70.7-57.3-128-128-128z"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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