[parentId => 'father'|'mother'] $children = []; // parentId => [childId, ...] $spouses = []; // id => [spouseId, ...] $st = Database::get()->prepare( 'SELECT from_id, to_id, kind, from_role FROM relations WHERE family_id = ?' ); $st->execute([$familyId]); foreach ($st->fetchAll() as $r) { $f = (int)$r['from_id']; $t = (int)$r['to_id']; if ($r['kind'] === 'parent') { $role = $r['from_role'] === 'mother' ? 'mother' : 'father'; $parents[$t][$f] = $role; $children[$f][] = $t; } else { $spouses[$f][] = $t; $spouses[$t][] = $f; } } foreach ($children as $k => $v) { $children[$k] = array_values(array_unique($v)); } return ['parents' => $parents, 'children' => $children, 'spouses' => $spouses]; } /** 某人的父母(father / mother 两个键,可能缺) */ public static function parents(array $g, int $id): array { $out = ['father' => null, 'mother' => null]; foreach ($g['parents'][$id] ?? [] as $pid => $role) { $out[$role] = (int)$pid; } return $out; } /** * 父母的 id 列表(不含 null),判断「跟我有关系」时用。 * * ⚠️ v14:这里**直接读图里的边**,不再绕 `parents()` 那对 father/mother 槽位。 * 绕槽位会**丢人**:`parents()` 是按角色往两个槽里填的,一旦数据里 * 同一个角色被记了两个人(典型:母亲那条边漏填 from_role,于是"父亲"这个 * 槽被母亲覆盖/挤掉),`parentIds()` 就只剩一个人 —— * 下游的家谱图于是只剩一位家长,**母亲那边整条线消失**(正是用户报的那个 * 「母亲登录进去看不到自己的孩子」的另一种成因)。 */ public static function parentIds(array $g, int $id): array { $out = []; foreach (array_keys($g['parents'][$id] ?? []) as $pid) { $out[] = (int)$pid; } return $out; } public static function spouseIds(array $g, int $id): array { return $g['spouses'][$id] ?? []; } /** 兄弟姐妹:与「我」共享至少一位父母的人(不含自己) */ public static function siblingIds(array $g, int $id): array { $sib = []; foreach ($g['parents'][$id] ?? [] as $pid => $_) { foreach ($g['children'][$pid] ?? [] as $c) { if ((int)$c !== $id) { $sib[(int)$c] = true; } } } return array_keys($sib); } /** 子女 */ public static function childIds(array $g, int $id): array { return array_map('intval', $g['children'][$id] ?? []); } /** * 「我」的血亲集合:只走 parent / child 边能到达的人(**不含配偶边**)。 * * 用途:家谱页判断某一户是不是「姻亲」。定义就取本家语义 —— * 我妻子的父母只走血缘边从「我」出发是走不到的,所以是姻亲; * 我妻子本人也一样走不到(她是配偶,不是血亲)。 * * ⚠️ 用 BFS 并带上 $seen 防环:数据脏了(互为父母、自己给自己当父母)时 * 不会死循环 —— 这类脏数据在手工录入姓名和关系时真的会进来。 */ private static function bloodRelatives(array $g, int $me): array { $out = [$me => true]; $q = [$me]; while ($q) { $id = array_shift($q); $nbrs = array_merge(self::parentIds($g, $id), self::childIds($g, $id)); foreach ($nbrs as $n) { $n = (int)$n; if (!isset($out[$n])) { $out[$n] = true; $q[] = $n; } } } return array_map('intval', array_keys($out)); } /** * 祖父母那一级:往上走一步再取父母。 * 只认「我的父母」,因为只有相邻两代有边 —— 这正是「只存两代」的兑现处。 */ public static function grandparents(array $g, int $id): array { $out = ['paternal' => ['father' => null, 'mother' => null], 'maternal' => ['father' => null, 'mother' => null]]; $p = self::parents($g, $id); if ($p['father']) { $out['paternal'] = self::parents($g, $p['father']); } if ($p['mother']) { $out['maternal'] = self::parents($g, $p['mother']); } return $out; } /** * 我该怎么称呼对方 —— 整个系统的「以我为中心」就落在这个函数上。 * * 推导顺序:自己 → 配偶 → 父母/子女 → 兄弟姐妹 → 父母的兄弟姐妹 → * 配偶的父母 → 祖父母/孙辈 → 姻亲 → 兜底「亲属」 */ public static function label(array $g, int $me, int $other, array $people): string { if ($me === $other) { return '我'; } $gender = strtolower((string)($people[$other]['gender'] ?? 'x')); $isMale = $gender === 'm'; $isKnown = $gender === 'm' || $gender === 'f'; // ---- 配偶 ---- if (in_array($other, self::spouseIds($g, $me), true)) { return self::spouseLabel($g, $me, $isMale); } // ---- 直系:父母 / 子女 ---- $myParents = self::parents($g, $me); foreach (['father', 'mother'] as $role) { if ($myParents[$role] === $other) { return $role === 'father' ? '父亲' : '母亲'; } } if (in_array($other, self::childIds($g, $me), true)) { return self::rankLabel($g, $people, $me, $other, $isMale, $isKnown); } // ---- 兄弟姐妹(含排行)---- foreach (self::siblingIds($g, $me) as $sib) { if ($sib === $other) { // compareRank(我, 他) < 0 表示他的排行键更小 —— 也就是他比我年长。 // 方向很容易写反:这里比的是「他 vs 我」,不是「我 vs 他」。 $heIsOlder = self::compareRank($people, $other, $me) < 0; if ($isKnown) { if ($isMale) { return $heIsOlder ? '哥哥' : '弟弟'; } return $heIsOlder ? '姐姐' : '妹妹'; } return $heIsOlder ? '哥哥' : '弟弟'; } } // ---- 父母的兄弟姐妹:伯/叔/姑/舅/姨 ---- $uncle = self::uncleLabel($g, $people, $me, $other); if ($uncle !== null) { return $uncle; } // ---- 配偶的父母:公公婆婆 / 岳父岳母 ---- foreach (self::spouseIds($g, $me) as $sp) { $spParents = self::parents($g, $sp); foreach (['father', 'mother'] as $role) { if ($spParents[$role] === $other) { $iAmMale = strtolower((string)($people[$me]['gender'] ?? 'x')) === 'm'; if ($role === 'father') { return $iAmMale ? '岳父' : '公公'; } return $iAmMale ? '岳母' : '婆婆'; } } } // ---- 祖父母 / 外祖父母 ---- $gp = self::grandparents($g, $me); foreach (['paternal' => '爷爷', 'maternal' => '外公'] as $side => $maleWord) { $femaleWord = $side === 'paternal' ? '奶奶' : '外婆'; if (($gp[$side]['father'] ?? null) === $other) { return $maleWord; } if (($gp[$side]['mother'] ?? null) === $other) { return $femaleWord; } } // ---- 孙辈 / 曾孙辈 ---- // 只往下走两级:孙辈(子女的子女)与曾孙辈(子女的孙辈)。 // 再往下就不必细分了,统一「后代」。 foreach (self::childIds($g, $me) as $child) { foreach (self::childIds($g, $child) as $gc) { if ($gc === $other) { if (!$isKnown) { return '孙辈'; } return $isMale ? '孙子' : '孙女'; } if (in_array($other, self::childIds($g, $gc), true)) { if (!$isKnown) { return '后代'; } return $isMale ? '曾孙' : '曾孙女'; } } } // ---- 兄弟姐妹的子女:侄子/侄女(兄弟的孩子)· 外甥/外甥女(姐妹的孩子)---- // // v11 新增。这里原来会一路落到兜底的「亲属」—— 家谱第二层里, // 我兄弟姐妹的孩子就混在「亲属」里,用户明确要求「外甥、侄子按关系完全明确下来」。 // 判据是**孩子父母(我的兄弟姐妹)的性别**,不是孩子的性别: // 兄弟的孩子 = 侄子/侄女;姐妹的孩子 = 外甥/外甥女。 // 与排行无关(这里不分长幼)。 foreach (self::siblingIds($g, $me) as $sib) { if (!in_array($other, self::childIds($g, $sib), true)) { continue; } $sibGender = strtolower((string)($people[$sib]['gender'] ?? 'x')); if ($sibGender === 'm') { if (!$isKnown) { return '侄辈'; } return $isMale ? '侄子' : '侄女'; } if ($sibGender === 'f') { if (!$isKnown) { return '外甥辈'; } return $isMale ? '外甥' : '外甥女'; } return '侄辈'; // 连兄弟姐妹的性别都没填:不瞎猜,给个中性的 } // ---- 姻亲①:配偶的兄弟姐妹 ---- // // v11 起**按长幼定死**,不再输出「大舅子/小舅子」这种斜杠并列(用户明确要求)。 // 长幼比较的对象是**我的配偶**(不是我自己):比配偶大 = 大X子/大X姨… // 用哪一套词,取决于「我随谁叫」: // · 我(男)随妻子叫 → 她的兄弟 大/小舅子、她的姐妹 大/小姨子 // · 我(女)随丈夫叫 → 他的兄弟 大/小叔子、他的姐妹 大/小姑子 // 我没填性别时退而看配偶性别推断(异性夫妻的常见情形);两个都不知道才给「姻亲」。 $myGender = strtolower((string)($people[$me]['gender'] ?? 'x')); foreach (self::spouseIds($g, $me) as $sp) { if (!in_array($other, self::siblingIds($g, $sp), true)) { continue; } if (!$isKnown) { return '姻亲'; } $spGender = strtolower((string)($people[$sp]['gender'] ?? 'x')); $older = self::compareRank($people, $other, $sp) < 0; // 比我的配偶年长 $byWife = ($myGender === 'm') || ($myGender === 'x' && $spGender === 'f'); $byHusband= ($myGender === 'f') || ($myGender === 'x' && $spGender === 'm'); if ($byWife) { return $isMale ? ($older ? '大舅子' : '小舅子') : ($older ? '大姨子' : '小姨子'); } if ($byHusband) { return $isMale ? ($older ? '大伯子' : '小叔子') : ($older ? '大姑子' : '小姑子'); } return '姻亲'; } // ---- 姻亲②:兄弟姐妹的配偶(姐夫/妹夫 · 嫂子/弟媳)---- // // v11 起按**我那个兄弟姐妹相对我的长幼**定死(用户原话:「既然知道了每一代的长幼顺序, // 那么谁是姐姐,谁是妹妹是很明确的事情,是妹夫就是妹夫,是姐夫就是姐夫」)。 foreach (self::siblingIds($g, $me) as $sib) { if (!in_array($other, self::spouseIds($g, $sib), true)) { continue; } if (!$isKnown) { return '姻亲'; } $sibOlder = self::compareRank($people, $sib, $me) < 0; // 我的这个兄弟姐妹比我年长 if ($isMale) { return $sibOlder ? '姐夫' : '妹夫'; } return $sibOlder ? '嫂子' : '弟媳'; } // ---- 堂亲 / 表亲:父母的兄弟姐妹的子女 ---- // v11 起把「堂」与「表」分开 —— 旧写法一律「表亲」,把父亲兄弟的孩子 // (同姓同宗,是**堂**兄弟姐妹)说成表亲,辈分上没错但宗族关系是错的。 foreach (self::parentSiblings($g, $me) as $ps) { if (!in_array($other, self::childIds($g, $ps), true)) { continue; } $psGender = strtolower((string)($people[$ps]['gender'] ?? 'x')); // 只有「父亲这一侧的兄弟」的孩子才是堂亲 $isPaternalUncle = $psGender === 'm' && in_array($ps, self::siblingIds($g, $myParents['father'] ?? 0), true); if ($isPaternalUncle) { return $isKnown ? ($isMale ? '堂兄弟' : '堂姐妹') : '堂亲'; } return $isKnown ? ($isMale ? '表兄弟' : '表姐妹') : '表亲'; } return '亲属'; } /** 配偶的称呼:按「我的性别」定,因为中文里丈夫/妻子是从自己视角说的 */ private static function spouseLabel(array $g, int $me, bool $otherIsMale): string { if ($otherIsMale) { return '丈夫'; } return '妻子'; } /** * 子女的排行称呼:长子/次子/三子…,长女/次女/三女… * 优先用 persons.birth_rank;没填就按生日或 id 推。 * * ⚠️ $isMale 必须由调用方按 gender 传入 —— 早期这里写死成「子」, * 结果女儿一律显示成「次子」,是很容易漏的一类 bug。 * 性别未知时退化成中性的「孩子」,不要瞎猜成儿子。 */ private static function rankLabel(array $g, array $people, int $parent, int $child, bool $isMale, bool $known): string { $base = $known ? ($isMale ? '子' : '女') : '孩子'; $sibs = self::childIds($g, $parent); if (count($sibs) <= 1) { return $known ? ($isMale ? '儿子' : '女儿') : '孩子'; } $rank = self::rankOf($people, $sibs, $child); $prefix = ['', '长', '次', '三', '四', '五', '六', '七', '八', '九'][$rank] ?? ($rank . ''); return $prefix === '' ? $base : $prefix . $base; } /** 我在兄弟姐妹里排第几(1 起) */ private static function rankOf(array $people, array $sibs, int $id): int { $sorted = $sibs; usort($sorted, function ($a, $b) use ($people) { $ra = self::rankValue($people, $a); $rb = self::rankValue($people, $b); return $ra <=> $rb; }); $pos = array_search($id, $sorted, true); return $pos === false ? 1 : $pos + 1; } /** * 排序用的键(对外版)。 * * 家谱页要按它给同一代排长幼,而模板拿不到私有方法 —— 与其在模板里各写一套 * 「birth_rank → birthday → id」的兜底(写一次就会跟这里不一致),不如开个口子。 */ public static function rankValueFor(array $people, int $id): string { return self::rankValue($people, $id); } /** 排序用的键:显式 birth_rank 优先,否则用生日,再否则用 id */ private static function rankValue(array $people, int $id): string { $p = $people[$id] ?? []; if (!empty($p['birth_rank'])) { return sprintf('%06d', (int)$p['birth_rank']); } if (!empty($p['birthday'])) { return '5' . (string)$p['birthday']; } return '9' . sprintf('%09d', $id); } /** 谁是哥哥谁是弟弟:比较排行键 */ private static function compareRank(array $people, int $a, int $b): int { return strcmp(self::rankValue($people, $a), self::rankValue($people, $b)); } /** 父母的兄弟姐妹(伯父/叔叔/姑姑/舅舅/姨妈) */ private static function uncleLabel(array $g, array $people, int $me, int $other): ?string { $sibs = []; $myParents = self::parents($g, $me); $paternal = $myParents['father'] !== null; foreach (['father' => true, 'mother' => false] as $role => $isPat) { $pid = $myParents[$role]; if ($pid === null) { continue; } foreach (self::siblingIds($g, $pid) as $sib) { $sibs[$sib] = ['side' => $isPat ? 'paternal' : 'maternal', 'parent' => $pid]; } } if (!isset($sibs[$other])) { return null; } $side = $sibs[$other]['side']; $parent = $sibs[$other]['parent']; $gender = strtolower((string)($people[$other]['gender'] ?? 'x')); $isMale = $gender === 'm'; $older = self::compareRank($people, $other, $parent) < 0; // 比我的父母年长 if ($side === 'paternal') { if ($isMale) { return $older ? '伯父' : '叔叔'; } return $gender === 'f' ? '姑姑' : '姑姑'; } if ($isMale) { return '舅舅'; } return $gender === 'f' ? '姨妈' : '姨妈'; } /** 父母的兄弟姐妹(不含配偶那边的) */ private static function parentSiblings(array $g, int $me): array { $out = []; foreach (self::parents($g, $me) as $pid) { if ($pid === null) { continue; } foreach (self::siblingIds($g, $pid) as $sib) { $out[] = $sib; } } return $out; } // ================= 家谱渲染所需的结构化数据 ================= /** * v14:**以我为中心,把有关系的都检索出来**,只保留「上一代 / 这一代 / 下一代」。 * * 为什么重写(用户 2026-10-01 第 5 条,原话「显示不齐全…应该以本人为中心去搜索, * 只要有关系的,都检索出来进行展示」): * 旧写法是「从我出发上下各 N 步」的**步数**限制,而真正要的是**代数**限制。 * 两者在「横向」关系上差很远:我的堂兄弟要先**上一步**到伯父、**再下一步**才到, * 两步就耗尽了配额,于是「这一代」里只剩亲兄弟姐妹,堂表亲全不见了。 * 改成按代数筛选之后,横向绕多远都不算远 —— 只要落回这三代里就要。 * * 三条边都要走(parent / child / spouse),缺一条就会漏人: * · 只走上下 → 配偶、嫂子弟媳、姐夫妹夫全没了; * · 不走配偶 → 连「我配偶的兄弟姐妹」都到不了(要先经过配偶这个人)。 * * ⚠️ 用 BFS + $gen 防环:数据脏了(互为父母、自己给自己当父母)不会死循环。 * ⚠️ 只把「落在 [-up, +down] 里的人」入队 —— 超出三代的不展开, * 但仍要允许**经过**上一代去够到同代的堂表亲(所以筛选的是代数,不是步数)。 * * @return array personId => 相对我的代数(-1 上一代 / 0 这一代 / +1 下一代) */ public static function focusMap(array $g, int $me, array $people, int $up = 1, int $down = 1): array { if (!isset($people[$me])) { return []; } $gen = [$me => 0]; $queue = [$me]; while ($queue) { $id = array_shift($queue); $lv = (int)$gen[$id]; $add = static function (int $nid, int $nlv) use (&$gen, &$queue, $people, $up, $down): void { if ($nlv < -$up || $nlv > $down) { return; // 落在三代之外:不收、也不从它继续展开 } if (!isset($people[$nid]) || isset($gen[$nid])) { return; // 名册里没有 / 已经有更靠前的走法 } $gen[$nid] = $nlv; $queue[] = $nid; }; // 配偶永远同代 foreach (self::spouseIds($g, $id) as $sp) { $add((int)$sp, $lv); } foreach (self::parentIds($g, $id) as $pid) { $add((int)$pid, $lv - 1); } foreach (self::childIds($g, $id) as $cid) { $add((int)$cid, $lv + 1); } } return $gen; } /** * v14:**子女必须挂在「夫妻」上,而不是挂在其中一个人身上**。 * * 用户点名的重大 bug(原话):「如果从一个出来,例如从父亲出来,母亲登录进去之后 * 竟然看不到自己的孩子」。根因不是显示,是**数据**:只要某个孩子的 parents 只写了 * 父亲一个人,那张图对母亲来说就是"别人的孩子"。 * * 所以这里做一次**对称补齐**:父母双方互为配偶时,孩子同时挂到两个人身上, * 两个人的 children 里也都有这个孩子。库(family-chart)在「孩子有两位父母、 * 且这两位互为配偶」时,会把连线从**夫妻之间那条横线**的中点拉下来 —— * 这正是用户要的效果,也顺手解决了"母亲看不到孩子"。 * * ⚠️ 只补**真实存在的**父/母(relations 里有的边),绝不凭空造人。 * * @param array $ids 图上会出现的人(过滤悬空引用用) * @return array{parents:array,children:array,spouses:array} */ public static function normalizeLinks(array $g, array $ids): array { $onTree = array_fill_keys(array_map('intval', array_values($ids)), true); $parents = []; $children = []; $spouses = []; foreach (array_keys($onTree) as $id) { $id = (int)$id; // ---- 配偶 ---- $sp = []; foreach (self::spouseIds($g, $id) as $s) { $s = (int)$s; if (isset($onTree[$s]) && !in_array($s, $sp, true)) { $sp[] = $s; } } $spouses[$id] = $sp; // ---- 父母 ---- // ⚠️ 一律取**图里所有的 parent 边**,绝不只取 father/mother 两个槽位 —— // 槽位会丢人(见 parentIds 的注释)。 $pa = []; foreach (self::parentIds($g, $id) as $p) { $p = (int)$p; if (isset($onTree[$p]) && !in_array($p, $pa, true)) { $pa[] = $p; } } $parents[$id] = $pa; } // ---- 子女:由 parents 反推,天然对称 ---- foreach ($parents as $child => $pa) { foreach ($pa as $p) { $children[$p][] = (int)$child; } } foreach ($children as $k => $v) { $v = array_values(array_unique(array_map('intval', $v))); usort($v, fn($a, $b) => $a <=> $b); $children[$k] = $v; } return ['parents' => $parents, 'children' => $children, 'spouses' => $spouses]; } /** * 以 $me 为中心,向上向下各展开若干代,产出可渲染的家谱树。 * * 返回结构: * [ * 'generations' => [ -2 => [id,...], -1 => [...], 0 => [...], 1 => [...] ], * —— 同代的人,**夫妻相邻**(旧版模板 people/show 的迷你家谱仍按它渲染) * 'units' => [ level => [ 夫妻单元, ... ] ], * —— v11 家谱页按「夫妻单元」渲染。单元 = 这一代里的一户: * ids[0] 是主位(血亲),ids[1] 是配偶,配偶排在家谱上主位的右手边。 * 单元带 parent_key —— 上一代哪个单元是它的父母, * 家谱页据此画「谁是谁的父母与孩子」的括号连线。 * 'unit_names' => [ 单元key => ['head'=>名, 'spouse'=>名|null] ],连线标题用 * 'nodes' => [ id => ['id','name','label','rel','unit','is_head',...] ] * 'gen' => [ id => 相对代数 ] * ] */ public static function tree(array $g, int $me, array $people, int $up = 3, int $down = 3): array { $gen = []; // id => 相对我的代数(我 = 0,父母 = -1,子女 = +1) $seen = []; $queue = [[$me, 0]]; $seen[$me] = true; while ($queue) { [$id, $level] = array_shift($queue); // 配偶永远同代 foreach (self::spouseIds($g, $id) as $sp) { if (!isset($seen[$sp])) { $seen[$sp] = true; $gen[$sp] = $level; $queue[] = [$sp, $level]; } } // 往上 if ($level > -$up) { foreach (self::parents($g, $id) as $pid) { if ($pid !== null && !isset($seen[$pid])) { $seen[$pid] = true; $gen[$pid] = $level - 1; $queue[] = [$pid, $level - 1]; } } } // 往下 if ($level < $down) { foreach (self::childIds($g, $id) as $cid) { if (!isset($seen[$cid])) { $seen[$cid] = true; $gen[$cid] = $level + 1; $queue[] = [$cid, $level + 1]; } } } } $gen[$me] = 0; // 按代数分堆(只留名册里真的有的人) $byGen = []; foreach ($gen as $id => $level) { if (!isset($people[$id])) { continue; } $byGen[$level][] = (int)$id; } ksort($byGen); // ---- 逐代建「夫妻单元」→ 按长幼排序 → 记录单元位置,供下一代定位父母 ---- $unitsByLevel = []; $unitOrder = []; // 单元 key => 该代内的下标(第几个单元,从左到右) $nodeUnit = []; // person id => 单元 key foreach (array_keys($byGen) as $level) { $units = self::buildUnits($g, $people, $byGen[$level], $gen); foreach ($units as &$u) { $u['key'] = $level . ':' . $u['head']; foreach ($u['ids'] as $m) { $nodeUnit[$m] = $u['key']; } // 上一代里哪一户是它的父母(优先父、其次母)—— 连线挂到这一户上 $u['parent_key'] = self::bracketParent($g, $gen, $u['head'], $nodeUnit); $u['parent_idx'] = $u['parent_key'] !== null ? ($unitOrder[$u['parent_key']] ?? PHP_INT_MAX) : -1; } unset($u); // 排序:先按「父母那一户从左到右的位置」(同一对父母的孩子会挨在一起, // 家谱页才能用一个括号把它们连到父母那一户),再按长幼(长子左、次子右)。 usort($units, function ($a, $b) use ($people) { if ($a['parent_idx'] !== $b['parent_idx']) { return $a['parent_idx'] <=> $b['parent_idx']; } $c = strcmp(self::rankValue($people, $a['head']), self::rankValue($people, $b['head'])); return $c !== 0 ? $c : ($a['head'] <=> $b['head']); }); foreach ($units as $i => $u) { $unitOrder[$u['key']] = $i; } $unitsByLevel[$level] = $units; } // ---- 谁是「姻亲」(配偶那边漂在树上的旁支)---- // // 判据:这一户**不是我的血亲**。血亲 = 只走 parent/child 边能从「我」走到的人 // (不含配偶边)—— 这正是「本家」的定义,也和模板图例那句话一一对应。 // // ⚠️ 这里原来用的是「上无父母 且 下无子女单元」,对**我妻子的父母**恒为假: // 岳父母在我的树里当然有子女(我妻子就在树上),于是永远标不上「姻亲」, // 而模板注释里点名的例子恰好就是「我妻子的父母」。实测(_fb_probe_kin_units.py) // 那一户真的没标、还排在血亲左边。改成血亲可达性之后语义才对得上。 $blood = self::bloodRelatives($g, $me); foreach ($unitsByLevel as $level => &$units) { foreach ($units as &$u) { $u['inlaw'] = !in_array((int)$u['head'], $blood, true) && !($u['spouse'] !== null && in_array((int)$u['spouse'], $blood, true)); } unset($u); // 姻亲排到本代**末尾**:血亲从左到右是按长幼的,姻亲插在中间会把 // 「长子/次子」的读数打乱(岳父母排到我父亲左边,看着像我家长辈)。 // 用两趟分区而不是 asort —— asort 在键相同(都是 false)时会打乱相对顺序, // 而这里血亲的相对顺序(即长幼)是必须原样保留的。 $bloodU = []; $inlawU = []; foreach ($units as $u) { if ($u['inlaw']) { $inlawU[] = $u; } else { $bloodU[] = $u; } } $units = array_merge($bloodU, $inlawU); foreach ($units as $i => $u) { $unitOrder[$u['key']] = $i; } } unset($units); // ---- 摊平成渲染用的数据 ---- $generations = []; $nodes = []; $unitNames = []; foreach ($unitsByLevel as $level => $units) { $generations[$level] = []; foreach ($units as $u) { $hn = (string)($people[$u['head']]['name'] ?? ''); $sn = $u['spouse'] !== null ? (string)($people[$u['spouse']]['name'] ?? '') : null; $unitNames[$u['key']] = ['head' => $hn, 'spouse' => $sn]; foreach ($u['ids'] as $k => $id) { $generations[$level][] = $id; $p = $people[$id]; $nodes[$id] = [ 'id' => (int)$id, 'name' => (string)$p['name'], 'label' => self::label($g, $me, (int)$id, $people), 'rel' => self::relationWord($g, $me, (int)$id), 'avatar' => (string)($p['avatar'] ?? ''), 'gender' => (string)($p['gender'] ?? 'x'), 'ph' => (int)($p['is_placeholder'] ?? 0) === 1, 'unit' => $u['key'], 'is_head' => $k === 0, 'person' => $p, ]; } } } return [ 'generations' => $generations, 'units' => $unitsByLevel, 'unit_names' => $unitNames, 'nodes' => $nodes, 'gen' => $gen, ]; } /** * 把同一代的人聚成「夫妻单元」:夫妻并排(男左女右),单身自成单元。 * * 单元的 ids[0] 是**主位** —— 优先给「在树里往上还有父母的那一个」(血亲), * 因为连线是从主位往上接到父母那一户的。都/都不连上一代时按男左女右; * 同性或性别未知就退而让排行靠前的当主位。 * * @param int[] $ids 同一代的人 * @return array */ private static function buildUnits(array $g, array $people, array $ids, array $gen): array { $ids = array_values(array_unique(array_map('intval', $ids))); $inGen = array_fill_keys($ids, true); $used = []; $units = []; $hasUp = static function (int $id) use ($g, $gen): bool { foreach (self::parentIds($g, $id) as $p) { if (isset($gen[$p]) && (int)$gen[$p] === (int)$gen[$id] - 1) { return true; } } return false; }; $isMale = static function (int $id) use ($people): bool { return strtolower((string)($people[$id]['gender'] ?? 'x')) === 'm'; }; foreach ($ids as $id) { if (isset($used[$id])) { continue; } $used[$id] = true; $spouse = null; foreach (self::spouseIds($g, $id) as $sp) { $sp = (int)$sp; if (isset($inGen[$sp]) && !isset($used[$sp])) { $spouse = $sp; break; } } $head = $id; $tail = null; if ($spouse !== null) { $used[$spouse] = true; $tail = $spouse; $idUp = $hasUp($id); $spUp = $hasUp($spouse); if ($spUp && !$idUp) { $head = $spouse; $tail = $id; } elseif ($idUp === $spUp) { if ($isMale($spouse) !== $isMale($id)) { if ($isMale($spouse)) { // 男左女右 $head = $spouse; $tail = $id; } } elseif (self::compareRank($people, $spouse, $id) < 0) { $head = $spouse; // 同性/未知:排行靠前的当主位 $tail = $id; } } } $unitIds = [$head]; if ($tail !== null) { $unitIds[] = $tail; } $units[] = ['ids' => $unitIds, 'head' => $head, 'spouse' => $tail]; } return $units; } /** 这个人的父母落在上一代的哪个单元里(优先父、其次母);没有就 null */ private static function bracketParent(array $g, array $gen, int $head, array $nodeUnit): ?string { $level = (int)($gen[$head] ?? 0) - 1; foreach (self::parents($g, $head) as $pid) { if ($pid !== null && isset($nodeUnit[$pid]) && (int)($gen[$pid] ?? 999) === $level) { return $nodeUnit[$pid]; } } return null; } /** 关系类型的「英文键」——给前端画线用,不受中文称呼影响 */ public static function relationWord(array $g, int $me, int $other): string { if ($me === $other) { return 'self'; } if (in_array($other, self::spouseIds($g, $me), true)) { return 'spouse'; } if (self::parents($g, $me)['father'] === $other || self::parents($g, $me)['mother'] === $other) { return 'parent'; } if (in_array($other, self::childIds($g, $me), true)) { return 'child'; } if (in_array($other, self::siblingIds($g, $me), true)) { return 'sibling'; } foreach (self::childIds($g, $me) as $c) { foreach (self::childIds($g, $c) as $gc) { if ($gc === $other || in_array($other, self::childIds($g, $gc), true)) { return 'grandchild'; } } } foreach (self::grandparents($g, $me) as $side) { if ($side['father'] === $other || $side['mother'] === $other) { return 'grandparent'; } } return 'kin'; } /** 把 id 列表变成 名字 => 称谓 的列表,供页面直接渲染 */ public static function describe(array $g, int $me, array $ids, array $people): array { $out = []; foreach ($ids as $id) { if (isset($people[$id])) { $out[] = [ 'id' => (int)$id, 'name' => (string)$people[$id]['name'], 'label' => self::label($g, $me, (int)$id, $people), ]; } } return $out; } }