Files
chinese-family-tree-2/lib/gedcom.ts
T
freedakgmail e44dd0bd95 0.0.8.0
2025-11-23 18:03:04 +08:00

342 lines
10 KiB
TypeScript

import type { FamilyMember, FamilyTreeData } from "@/types/family"
import { parseGedcom } from "read-gedcom"
/**
* 将 FamilyTreeData 导出为 GEDCOM 格式
*/
export function exportToGedcom(treeData: FamilyTreeData): string {
const lines: string[] = []
// GEDCOM Header
lines.push("0 HEAD")
lines.push("1 SOUR Chinese Family Tree")
lines.push("2 VERS 1.0")
lines.push("2 NAME 华夏谱")
lines.push("1 DEST ANY")
lines.push("1 DATE " + new Date().toISOString().split('T')[0].replace(/-/g, ' '))
lines.push("1 CHAR UTF-8")
lines.push("1 GEDC")
lines.push("2 VERS 5.5.1")
lines.push("2 FORM LINEAGE-LINKED")
const members = Object.values(treeData.members)
// 个人记录 (INDI)
members.forEach(member => {
lines.push(`0 @I${member.id}@ INDI`)
lines.push(`1 NAME ${member.fullName}`)
lines.push(`2 GIVN ${member.givenName}`)
lines.push(`2 SURN ${member.surname}`)
if (member.courtesyName) {
lines.push(`2 _AKA ${member.courtesyName}`)
}
if (member.gender === 'MALE') {
lines.push("1 SEX M")
} else if (member.gender === 'FEMALE') {
lines.push("1 SEX F")
}
// 出生信息
if (member.birthDate) {
lines.push("1 BIRT")
lines.push(`2 DATE ${formatGedcomDate(member.birthDate)}`)
if (member.birthPlace) {
lines.push(`2 PLAC ${member.birthPlace}`)
}
}
// 逝世信息
if (member.deathDate) {
lines.push("1 DEAT")
lines.push(`2 DATE ${formatGedcomDate(member.deathDate)}`)
if (member.burialPlace) {
lines.push(`2 PLAC ${member.burialPlace}`)
}
}
// 籍贯
if (member.ancestralHome) {
lines.push("1 RESI")
lines.push(`2 PLAC ${member.ancestralHome}`)
}
// 备注
if (member.bio) {
lines.push("1 NOTE " + member.bio.replace(/\n/g, "\n1 CONT "))
}
// 家庭关系 - 作为子女
if (member.fatherId || member.motherId) {
const famId = `F${member.fatherId || member.motherId}_${member.id}`
lines.push(`1 FAMC @${famId}@`)
}
// 家庭关系 - 作为配偶
if (member.spouseIds.length > 0) {
member.spouseIds.forEach(spouseId => {
const famId = `F${member.id}_${spouseId}`
lines.push(`1 FAMS @${famId}@`)
})
}
})
// 家庭记录 (FAM)
const processedFamilies = new Set<string>()
members.forEach(member => {
// 创建父母家庭
if (member.fatherId || member.motherId) {
const famId = `F${member.fatherId || member.motherId}_${member.id}`
if (!processedFamilies.has(famId)) {
lines.push(`0 @${famId}@ FAM`)
if (member.fatherId) {
lines.push(`1 HUSB @I${member.fatherId}@`)
}
if (member.motherId) {
lines.push(`1 WIFE @I${member.motherId}@`)
}
// 查找所有同父母的子女
const siblings = members.filter(m =>
m.fatherId === member.fatherId && m.motherId === member.motherId
)
siblings.forEach(child => {
lines.push(`1 CHIL @I${child.id}@`)
})
processedFamilies.add(famId)
}
}
// 创建配偶家庭
if (member.spouseIds.length > 0) {
member.spouseIds.forEach(spouseId => {
const famId = member.gender === 'MALE'
? `F${member.id}_${spouseId}`
: `F${spouseId}_${member.id}`
if (!processedFamilies.has(famId)) {
lines.push(`0 @${famId}@ FAM`)
if (member.gender === 'MALE') {
lines.push(`1 HUSB @I${member.id}@`)
lines.push(`1 WIFE @I${spouseId}@`)
} else {
lines.push(`1 HUSB @I${spouseId}@`)
lines.push(`1 WIFE @I${member.id}@`)
}
// 添加子女
member.childrenIds.forEach(childId => {
lines.push(`1 CHIL @I${childId}@`)
})
processedFamilies.add(famId)
}
})
}
})
// GEDCOM Trailer
lines.push("0 TRLR")
return lines.join("\n")
}
/**
* 从 GEDCOM 格式导入数据
*/
export function importFromGedcom(gedcomText: string): FamilyTreeData {
const parsed = parseGedcom(gedcomText as any)
const members: Record<string, FamilyMember> = {}
let rootId: string | undefined
// 第一遍:创建所有个人
const records = Array.isArray(parsed) ? parsed : (parsed as any).tree || []
records.forEach((record: any) => {
if (record.tag === 'INDI') {
const id = record.pointer.replace('@I', '').replace('@', '')
let fullName = ''
let givenName = ''
let surname = ''
let courtesyName: string | undefined
let gender: 'MALE' | 'FEMALE' | 'OTHER' = 'OTHER'
let birthDate: string | undefined
let deathDate: string | undefined
let birthPlace: string | undefined
let burialPlace: string | undefined
let ancestralHome: string | undefined
let bio: string | undefined
record.tree.forEach((field: any) => {
if (field.tag === 'NAME') {
fullName = field.data.replace(/\//g, '').trim()
field.tree?.forEach((subField: any) => {
if (subField.tag === 'GIVN') givenName = subField.data
if (subField.tag === 'SURN') surname = subField.data
if (subField.tag === '_AKA') courtesyName = subField.data
})
}
if (field.tag === 'SEX') {
gender = field.data === 'M' ? 'MALE' : field.data === 'F' ? 'FEMALE' : 'OTHER'
}
if (field.tag === 'BIRT') {
field.tree?.forEach((subField: any) => {
if (subField.tag === 'DATE') birthDate = parseGedcomDate(subField.data)
if (subField.tag === 'PLAC') birthPlace = subField.data
})
}
if (field.tag === 'DEAT') {
field.tree?.forEach((subField: any) => {
if (subField.tag === 'DATE') deathDate = parseGedcomDate(subField.data)
if (subField.tag === 'PLAC') burialPlace = subField.data
})
}
if (field.tag === 'RESI') {
field.tree?.forEach((subField: any) => {
if (subField.tag === 'PLAC') ancestralHome = subField.data
})
}
if (field.tag === 'NOTE') {
bio = field.data
}
})
members[id] = {
id,
surname: surname || '未知',
givenName: givenName || fullName,
fullName: fullName || `${surname}${givenName}`,
gender,
generation: 1, // 将在第二遍中计算
courtesyName,
birthDate,
deathDate,
birthPlace,
burialPlace,
ancestralHome,
bio,
spouseIds: [],
childrenIds: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
}
})
// 第二遍:建立关系
records.forEach((record: any) => {
if (record.tag === 'FAM') {
let husbId: string | undefined
let wifeId: string | undefined
const childIds: string[] = []
record.tree.forEach((field: any) => {
if (field.tag === 'HUSB') {
husbId = field.data.replace('@I', '').replace('@', '')
}
if (field.tag === 'WIFE') {
wifeId = field.data.replace('@I', '').replace('@', '')
}
if (field.tag === 'CHIL') {
childIds.push(field.data.replace('@I', '').replace('@', ''))
}
})
// 设置配偶关系
if (husbId && wifeId) {
if (members[husbId] && !members[husbId].spouseIds.includes(wifeId)) {
members[husbId].spouseIds.push(wifeId)
}
if (members[wifeId] && !members[wifeId].spouseIds.includes(husbId)) {
members[wifeId].spouseIds.push(husbId)
}
}
// 设置父母-子女关系
childIds.forEach(childId => {
if (members[childId]) {
if (husbId) {
members[childId].fatherId = husbId
if (members[husbId] && !members[husbId].childrenIds.includes(childId)) {
members[husbId].childrenIds.push(childId)
}
}
if (wifeId) {
members[childId].motherId = wifeId
if (members[wifeId] && !members[wifeId].childrenIds.includes(childId)) {
members[wifeId].childrenIds.push(childId)
}
}
}
})
}
})
// 计算世代并找到根节点
const calculateGeneration = (id: string, visited = new Set<string>()): number => {
if (visited.has(id)) return 1 // 防止循环引用
visited.add(id)
const member = members[id]
if (!member) return 1
if (!member.fatherId && !member.motherId) {
return 1 // 根节点
}
const fatherGen = member.fatherId ? calculateGeneration(member.fatherId, visited) : 0
const motherGen = member.motherId ? calculateGeneration(member.motherId, visited) : 0
return Math.max(fatherGen, motherGen) + 1
}
Object.keys(members).forEach(id => {
members[id].generation = calculateGeneration(id)
})
// 找到第一代作为根节点
rootId = Object.keys(members).find(id => members[id].generation === 1)
return {
members,
rootId,
}
}
// 辅助函数:格式化日期为 GEDCOM 格式
function formatGedcomDate(isoDate: string): string {
const date = new Date(isoDate)
const months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`
}
// 辅助函数:解析 GEDCOM 日期为 ISO 格式
function parseGedcomDate(gedcomDate: string): string {
const months: Record<string, number> = {
JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5,
JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11
}
const parts = gedcomDate.trim().split(' ')
if (parts.length === 3) {
const day = parseInt(parts[0])
const month = months[parts[1].toUpperCase()]
const year = parseInt(parts[2])
if (!isNaN(day) && month !== undefined && !isNaN(year)) {
return new Date(year, month, day).toISOString().split('T')[0]
}
}
// 如果只有年份
if (parts.length === 1 && !isNaN(parseInt(parts[0]))) {
return `${parts[0]}-01-01`
}
return new Date().toISOString().split('T')[0]
}