diff --git a/src/app.tsx b/src/app.tsx index 58b65ee..f923e80 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -12,6 +12,7 @@ import {Intro} from './intro'; import {Loader, Message, Portal, Responsive} from 'semantic-ui-react'; import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom'; import {TopBar} from './top_bar'; +import {loadWikiTree} from './wikitree'; /** Shows an error message in the middle of the screen. */ function ErrorMessage(props: {message?: string}) { @@ -117,11 +118,16 @@ export class App extends React.Component { hash: string | undefined, url: string | undefined, gedcom: string | undefined, + source: string | undefined, ): boolean { return ( !!(hash && hash !== this.state.hash) || !!(url && this.state.url !== url) || - (!!gedcom && !this.state.loading && !this.state.data) + (!!gedcom && !this.state.loading && !this.state.data) || + (source === 'wikitree' && + !this.state.loading && + !this.state.data && + !this.state.error) ); } @@ -226,6 +232,7 @@ export class App extends React.Component { const handleCors = getParam('handleCors') !== 'false'; // True by default. const standalone = getParam('standalone') !== 'false'; // True by default. const view = getParam('view'); + const source = getParam('source'); const chartTypes = new Map([ ['relatives', ChartType.Relatives], @@ -238,9 +245,9 @@ export class App extends React.Component { const images = this.props.location.state && this.props.location.state.images; - if (!url && !hash) { + if (!url && !hash && !source) { this.props.history.replace({pathname: '/'}); - } else if (this.isNewData(hash, url, gedcom)) { + } else if (this.isNewData(hash, url, gedcom, source)) { try { // Set loading state. this.setState( @@ -255,9 +262,12 @@ export class App extends React.Component { chartType, }), ); - const data = hash - ? await loadGedcom(hash, gedcom, images) - : await loadFromUrl(url!, handleCors); + const data = + source === 'wikitree' + ? await loadWikiTree(indi!, handleCors) + : hash + ? await loadGedcom(hash, gedcom, images) + : await loadFromUrl(url!, handleCors); const software = getSoftware(data.gedcom.head); analyticsEvent(hash ? 'upload_file_loaded' : 'url_file_loaded', { diff --git a/src/wikitree.ts b/src/wikitree.ts new file mode 100644 index 0000000..53e015c --- /dev/null +++ b/src/wikitree.ts @@ -0,0 +1,162 @@ +// WikiTree support is currenlty implemented only as proof of concept. +// It works for a specific id (12082793) and few others. + +import md5 from 'md5'; +import {loadGedcom} from './load_data'; + +interface GetAncestorsRequest { + action: 'getAncestors'; + key: string; + fields: string; +} + +interface GetRelatives { + action: 'getRelatives'; + keys: string; + getChildren?: true; + getSpouses?: true; +} + +type WikiTreeRequest = GetAncestorsRequest | GetRelatives; + +async function wikiTreeGet(request: WikiTreeRequest, handleCors: boolean) { + const requestData = new FormData(); + requestData.append('format', 'json'); + for (const key in request) { + requestData.append(key, request[key]); + } + const apiUrl = handleCors + ? 'https://cors-anywhere.herokuapp.com/https://apps.wikitree.com/api.php' + : 'https://apps.wikitree.com/api.php'; + const response = await window.fetch(apiUrl, { + method: 'POST', + body: requestData, + }); + const responseBody = await response.text(); + return JSON.parse(responseBody); +} + +function getFamilyId(id1: number, id2: number) { + if (id2 > id1) { + return `${id1}_${id2}`; + } + return `${id2}_${id1}`; +} + +export async function loadWikiTree(id: string, handleCors: boolean) { + const firstRelativesData = await wikiTreeGet( + { + action: 'getRelatives', + keys: id, + getChildren: true, + getSpouses: true, + }, + handleCors, + ); + + const spouseIds = Object.values( + firstRelativesData[0].items[0].person.Spouses, + ).map((s: any) => s.Id); + + const everyone: any[] = []; + + [id].concat(spouseIds).forEach(async (personId) => { + const ancestorsData = await wikiTreeGet( + { + action: 'getAncestors', + key: personId, + fields: '*', + }, + handleCors, + ); + const ancestors = ancestorsData[0].ancestors; + ancestors.forEach((a: any) => everyone.push(a)); + }); + + let toFetch = [id]; + + while (toFetch.length > 0) { + const relativesData = await wikiTreeGet( + { + action: 'getRelatives', + keys: toFetch.join(','), + getChildren: true, + getSpouses: true, + }, + handleCors, + ); + const people = relativesData[0].items.map((x: any) => x.person); + toFetch = []; + people.forEach((person: any) => { + everyone.push(person); + const spouses = Object.values(person.Spouses); + spouses.forEach((s) => everyone.push(s)); + const children = Object.values(person.Children); + const childrenKeys = (children as any[]).map((c) => c.Name); + childrenKeys.forEach((k) => toFetch.push(k)); + }); + } + + // Map from person id to the set of families where they are a spouse. + const families = new Map>(); + const children = new Map>(); + const spouses = new Map(); + function getSet(map: Map>, id: K): Set { + const set = map.get(id); + if (set) { + return set; + } + const newSet = new Set(); + map.set(id, newSet); + return newSet; + } + + everyone.forEach((person: any) => { + if (person.Mother || person.Father) { + const famId = getFamilyId(person.Mother, person.Father); + getSet(families, person.Mother).add(famId); + getSet(families, person.Father).add(famId); + getSet(children, famId).add(person.Id); + spouses.set(famId, { + wife: person.Mother || undefined, + husband: person.Father || undefined, + }); + } + }); + + const gedcomLines: string[] = ['0 HEAD']; + const converted = new Set(); + everyone.forEach((person: any) => { + if (converted.has(person.Id)) { + return; + } + converted.add(person.Id); + gedcomLines.push(`0 @${person.Id}@ INDI`); + gedcomLines.push(`1 NAME ${person.FirstName} /${person.LastNameAtBirth}/`); + if (person.Mother || person.Father) { + gedcomLines.push(`1 FAMC @${getFamilyId(person.Mother, person.Father)}@`); + } + // TODO: add to spouses map for each spouse. + getSet(families, person.Id).forEach((famId) => + gedcomLines.push(`1 FAMS @${famId}@`), + ); + }); + + spouses.forEach((value, key) => { + gedcomLines.push(`0 @${key}@ FAM`); + if (value.wife) { + gedcomLines.push(`1 WIFE @${value.wife}@`); + } + if (value.husband) { + gedcomLines.push(`1 HUSB @${value.husband}@`); + } + getSet(children, key).forEach((child) => { + gedcomLines.push(`1 CHIL @${child}@`); + }); + }); + gedcomLines.push('0 TRLR'); + const gedcom = gedcomLines.join('\n'); + + const hash = md5(gedcom); + return await loadGedcom(hash, gedcom); +}