Name types in details panel (#109)

This commit is contained in:
czifumasa
2022-08-30 15:58:39 +02:00
committed by GitHub
parent a92f06e43d
commit 0733690058
13 changed files with 168 additions and 35 deletions

12
package-lock.json generated
View File

@@ -5,7 +5,6 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "topola-viewer",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@artsy/fresnel": "^1.3.1", "@artsy/fresnel": "^1.3.1",
@@ -38,6 +37,7 @@
"semantic-ui-css": "^2.4.1", "semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.0.3", "semantic-ui-react": "^2.0.3",
"topola": "^3.5.0", "topola": "^3.5.0",
"turbocommons-ts": "^3.8.0",
"unified": "^10.1.0", "unified": "^10.1.0",
"wikitree-js": "^0.1.0" "wikitree-js": "^0.1.0"
}, },
@@ -22907,6 +22907,11 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/turbocommons-ts": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/turbocommons-ts/-/turbocommons-ts-3.8.0.tgz",
"integrity": "sha512-EQRCm2r944M/TqzfRutaCwTN+eHVLPAedaWF8catAHd49biN1UjB9CGLTBT2kI3i9lCccSt84s8YFf54u1WcAg=="
},
"node_modules/tweetnacl": { "node_modules/tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -44181,6 +44186,11 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"turbocommons-ts": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/turbocommons-ts/-/turbocommons-ts-3.8.0.tgz",
"integrity": "sha512-EQRCm2r944M/TqzfRutaCwTN+eHVLPAedaWF8catAHd49biN1UjB9CGLTBT2kI3i9lCccSt84s8YFf54u1WcAg=="
},
"tweetnacl": { "tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",

View File

@@ -33,6 +33,7 @@
"semantic-ui-css": "^2.4.1", "semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.0.3", "semantic-ui-react": "^2.0.3",
"topola": "^3.5.0", "topola": "^3.5.0",
"turbocommons-ts": "^3.8.0",
"unified": "^10.1.0", "unified": "^10.1.0",
"wikitree-js": "^0.1.0" "wikitree-js": "^0.1.0"
}, },

View File

@@ -14,12 +14,13 @@ import {GedcomEntry} from 'parse-gedcom';
import {IntlShape} from 'react-intl'; import {IntlShape} from 'react-intl';
import {TopolaError} from '../util/error'; import {TopolaError} from '../util/error';
import {isValidDateOrRange} from '../util/date_util'; import {isValidDateOrRange} from '../util/date_util';
import {StringUtils} from 'turbocommons-ts';
import { import {
getAncestors as getAncestorsApi, getAncestors as getAncestorsApi,
getRelatives as getRelativesApi, getRelatives as getRelativesApi,
clientLogin, clientLogin,
getLoggedInUserName, getLoggedInUserName,
Person Person,
} from 'wikitree-js'; } from 'wikitree-js';
/** Prefix for IDs of private individuals. */ /** Prefix for IDs of private individuals. */
@@ -66,7 +67,7 @@ async function getAncestors(
if (cachedData) { if (cachedData) {
return JSON.parse(cachedData); return JSON.parse(cachedData);
} }
const result = getAncestorsApi(key, {}, getApiOptions(handleCors)); const result = await getAncestorsApi(key, {}, getApiOptions(handleCors));
setSessionStorageItem(cacheKey, JSON.stringify(result)); setSessionStorageItem(cacheKey, JSON.stringify(result));
return result; return result;
} }
@@ -225,6 +226,12 @@ export async function loadWikiTree(
generation++; generation++;
} }
//Map from human-readable person id to person names
const personNames = new Map<
string,
{birth?: string; married?: string; aka?: string}
>();
// Map from person id to the set of families where they are a spouse. // Map from person id to the set of families where they are a spouse.
const families = new Map<number, Set<string>>(); const families = new Map<number, Set<string>>();
// Map from family id to the set of children. // Map from family id to the set of children.
@@ -268,6 +275,9 @@ export async function loadWikiTree(
`https://www.wikitree.com${person.PhotoData.path}`, `https://www.wikitree.com${person.PhotoData.path}`,
); );
} }
personNames.set(person.Name, convertPersonNames(person));
if (person.Spouses) { if (person.Spouses) {
Object.values(person.Spouses).forEach((spouse) => { Object.values(person.Spouses).forEach((spouse) => {
const famId = getFamilyId(person.Id, spouse.Id); const famId = getFamilyId(person.Id, spouse.Id);
@@ -314,7 +324,7 @@ export async function loadWikiTree(
}); });
const chartData = normalizeGedcom({indis, fams}); const chartData = normalizeGedcom({indis, fams});
const gedcom = buildGedcom(chartData, fullSizePhotoUrls); const gedcom = buildGedcom(chartData, fullSizePhotoUrls, personNames);
return {chartData, gedcom}; return {chartData, gedcom};
} }
@@ -387,6 +397,50 @@ function convertPerson(person: Person, intl: IntlShape): JsonIndi {
} }
return indi; return indi;
} }
/**
Resolve birth name, married name and aka name with following logic:
- birth name is always prioritized and is set if exists and is not unknown
- married name is based on LastNameCurrent and is set if it's different than birth name
and one of the spouses has it as their birth name
- aka name is based on LastNameOther and is set if it's different than others
*/
function convertPersonNames(person: Person) {
return {
birth:
person.LastNameAtBirth !== 'Unknown' ? person.LastNameAtBirth : undefined,
married:
person.Spouses &&
person.LastNameCurrent !== 'Unknown' &&
person.LastNameCurrent !== person.LastNameAtBirth &&
Object.entries(person.Spouses)
.flatMap(([, spousePerson]) =>
spousePerson.LastNameAtBirth.split(/[- ,]/),
)
.filter(
(spousePersonNamePart) =>
/* In some languages the same names can differ a bit between genders,
so regular equals comparison cannot be used.
To verify if spouse has the same name, person name is split to include people with double names,
then there is a check if any name part is at least 75% similar to spouse name.
*/
person.LastNameCurrent.split(/[- ,]/).filter(
(personNamePart) =>
StringUtils.compareSimilarityPercent(
spousePersonNamePart,
personNamePart,
) >= 75,
).length,
).length
? person.LastNameCurrent
: undefined,
aka:
person.LastNameOther !== 'Unknown' &&
person.LastNameAtBirth !== person.LastNameOther &&
person.LastNameCurrent !== person.LastNameOther
? person.LastNameOther
: undefined,
};
}
/** /**
* Parses a date in the format returned by WikiTree and converts in to * Parses a date in the format returned by WikiTree and converts in to
@@ -468,6 +522,24 @@ function dateOrRangeToGedcom(dateOrRange: DateOrRange): string {
return ''; return '';
} }
function nameToGedcom(type: string, firstName?: string, lastName?: string) {
return {
level: 1,
pointer: '',
tag: 'NAME',
data: `${firstName || ''} /${lastName || ''}/`,
tree: [
{
level: 2,
pointer: '',
tag: 'TYPE',
data: type,
tree: [],
},
],
};
}
function eventToGedcom(event: JsonEvent): GedcomEntry[] { function eventToGedcom(event: JsonEvent): GedcomEntry[] {
const result = []; const result = [];
if (isValidDateOrRange(event)) { if (isValidDateOrRange(event)) {
@@ -524,6 +596,7 @@ function imageToGedcom(
function indiToGedcom( function indiToGedcom(
indi: JsonIndi, indi: JsonIndi,
fullSizePhotoUrl: Map<string, string>, fullSizePhotoUrl: Map<string, string>,
personNames: {birth?: string; married?: string; aka?: string},
): GedcomEntry { ): GedcomEntry {
// WikiTree URLs replace spaces with underscores. // WikiTree URLs replace spaces with underscores.
const escapedId = indi.id.replace(/ /g, '_'); const escapedId = indi.id.replace(/ /g, '_');
@@ -532,16 +605,21 @@ function indiToGedcom(
pointer: `@${indi.id}@`, pointer: `@${indi.id}@`,
tag: 'INDI', tag: 'INDI',
data: '', data: '',
tree: [
{
level: 1,
pointer: '',
tag: 'NAME',
data: `${indi.firstName || ''} /${indi.lastName || ''}/`,
tree: [], tree: [],
},
],
}; };
if (personNames.birth) {
record.tree.push(nameToGedcom('birth', indi.firstName, personNames.birth));
}
if (personNames.married) {
record.tree.push(
nameToGedcom('married', indi.firstName, personNames.married),
);
}
if (personNames.aka) {
record.tree.push(nameToGedcom('aka', indi.firstName, personNames.aka));
}
if (indi.birth) { if (indi.birth) {
record.tree.push({ record.tree.push({
level: 1, level: 1,
@@ -653,11 +731,16 @@ function famToGedcom(fam: JsonFam): GedcomEntry {
function buildGedcom( function buildGedcom(
data: JsonGedcomData, data: JsonGedcomData,
fullSizePhotoUrls: Map<string, string>, fullSizePhotoUrls: Map<string, string>,
personNames: Map<string, {birth?: string; married?: string; aka?: string}>,
): GedcomData { ): GedcomData {
const gedcomIndis: {[key: string]: GedcomEntry} = {}; const gedcomIndis: {[key: string]: GedcomEntry} = {};
const gedcomFams: {[key: string]: GedcomEntry} = {}; const gedcomFams: {[key: string]: GedcomEntry} = {};
data.indis.forEach((indi) => { data.indis.forEach((indi) => {
gedcomIndis[indi.id] = indiToGedcom(indi, fullSizePhotoUrls); gedcomIndis[indi.id] = indiToGedcom(
indi,
fullSizePhotoUrls,
personNames.get(indi.id) || {},
);
}); });
data.fams.forEach((fam) => { data.fams.forEach((fam) => {
gedcomFams[fam.id] = famToGedcom(fam); gedcomFams[fam.id] = famToGedcom(fam);

View File

@@ -11,6 +11,7 @@ import {GedcomEntry} from 'parse-gedcom';
import {MultilineText} from './multiline-text'; import {MultilineText} from './multiline-text';
import {TranslatedTag} from './translated-tag'; import {TranslatedTag} from './translated-tag';
import {Header, Item} from 'semantic-ui-react'; import {Header, Item} from 'semantic-ui-react';
import {FormattedMessage} from 'react-intl';
import {WrappedImage} from './wrapped-image'; import {WrappedImage} from './wrapped-image';
const EXCLUDED_TAGS = [ const EXCLUDED_TAGS = [
@@ -83,18 +84,27 @@ function noteDetails(entry: GedcomEntry) {
} }
function nameDetails(entry: GedcomEntry) { function nameDetails(entry: GedcomEntry) {
const fullName = entry.data.replaceAll('/', '');
const nameType = entry.tree.find(
(entry) => entry.tag === 'TYPE' && entry.data !== 'Unknown',
)?.data;
return ( return (
<Header size="large"> <>
{entry.data <Header as="span" size="large">
.split('/') {fullName ? (
.filter((name) => !!name) fullName
.map((name, index) => ( ) : (
<div key={index}> <FormattedMessage id="name.unknown_name" defaultMessage="N.N." />
{name} )}
<br />
</div>
))}
</Header> </Header>
{fullName && nameType && (
<Item.Meta>
<TranslatedTag tag={nameType} />
</Item.Meta>
)}
</>
); );
} }

View File

@@ -5,7 +5,7 @@ import {compareDates, formatDateOrRange} from '../util/date_util';
import {DateOrRange, getDate} from 'topola'; import {DateOrRange, getDate} from 'topola';
import {dereference, GedcomData, getData, getName} from '../util/gedcom_util'; import {dereference, GedcomData, getData, getName} from '../util/gedcom_util';
import {GedcomEntry} from 'parse-gedcom'; import {GedcomEntry} from 'parse-gedcom';
import {IntlShape, useIntl} from 'react-intl'; import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
import {Link, useLocation} from 'react-router-dom'; import {Link, useLocation} from 'react-router-dom';
import {MultilineText} from './multiline-text'; import {MultilineText} from './multiline-text';
import {pointerToId} from '../util/gedcom_util'; import {pointerToId} from '../util/gedcom_util';
@@ -16,9 +16,6 @@ function PersonLink(props: {person: GedcomEntry}) {
const location = useLocation(); const location = useLocation();
const name = getName(props.person); const name = getName(props.person);
if (!name) {
return <></>;
}
const search = queryString.parse(location.search); const search = queryString.parse(location.search);
search['indi'] = pointerToId(props.person.pointer); search['indi'] = pointerToId(props.person.pointer);
@@ -26,7 +23,11 @@ function PersonLink(props: {person: GedcomEntry}) {
return ( return (
<Item.Meta> <Item.Meta>
<Link to={{pathname: '/view', search: queryString.stringify(search)}}> <Link to={{pathname: '/view', search: queryString.stringify(search)}}>
{name} {name ? (
name
) : (
<FormattedMessage id="name.unknown_name" defaultMessage="N.N." />
)}
</Link> </Link>
</Item.Meta> </Item.Meta>
); );

View File

@@ -22,6 +22,11 @@ const TAG_DESCRIPTIONS = new Map([
['OCCU', 'Occupation'], ['OCCU', 'Occupation'],
['TITL', 'Title'], ['TITL', 'Title'],
['WWW', 'WWW'], ['WWW', 'WWW'],
['birth', 'Birth name'],
['married', 'Married name'],
['maiden', 'Maiden name'],
['immigrant', 'Immigrant name'],
['aka', 'Also known as'],
]); ]);
interface Props { interface Props {

View File

@@ -6,10 +6,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
import {MenuItem, MenuType} from './menu_item'; import {MenuItem, MenuType} from './menu_item';
import {useEffect, useRef, useState} from 'react'; import {useEffect, useRef, useState} from 'react';
import {useHistory, useLocation} from 'react-router'; import {useHistory, useLocation} from 'react-router';
import { import {getLoggedInUserName, navigateToLoginPage} from 'wikitree-js';
getLoggedInUserName,
navigateToLoginPage,
} from 'wikitree-js';
interface Props { interface Props {
menuType: MenuType; menuType: MenuType;

View File

@@ -55,6 +55,10 @@
"gedcom.WWW": "Stránka WWW", "gedcom.WWW": "Stránka WWW",
"gedcom.RELI": "Vyznání", "gedcom.RELI": "Vyznání",
"gedcom._UPD": "Poslední aktualizace", "gedcom._UPD": "Poslední aktualizace",
"gedcom.birth": "Rodinné jméno",
"gedcom.married": "Manželské jméno",
"gedcom.maiden": "Jméno za svobodna",
"gedcom.aka": "Také znám(a) jako",
"date.abt": "kolem", "date.abt": "kolem",
"date.cal": "spočteno", "date.cal": "spočteno",
"date.est": "asi", "date.est": "asi",

View File

@@ -51,6 +51,10 @@
"gedcom.TITL": "Titel", "gedcom.TITL": "Titel",
"gedcom.WWW": "Website", "gedcom.WWW": "Website",
"gedcom._UPD": "Zuletzt aktualisiert", "gedcom._UPD": "Zuletzt aktualisiert",
"gedcom.birth": "Geburtsname",
"gedcom.married": "Ehenamen",
"gedcom.maiden": "Mädchenname",
"gedcom.aka": "Auch bekannt als",
"date.abt": "about", "date.abt": "about",
"date.cal": "berechnet", "date.cal": "berechnet",
"date.est": "geschätzt", "date.est": "geschätzt",

View File

@@ -50,6 +50,10 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Titre", "gedcom.TITL": "Titre",
"gedcom.WWW": "Site Web", "gedcom.WWW": "Site Web",
"gedcom.birth": "Nom de naissance",
"gedcom.married": "Nom marital",
"gedcom.maiden": "Nom de jeune fille",
"gedcom.aka": "Alias",
"gedcom._UPD": "Dernière mise à jour", "gedcom._UPD": "Dernière mise à jour",
"date.abt": "environ", "date.abt": "environ",
"date.cal": "calculé", "date.cal": "calculé",

View File

@@ -53,6 +53,10 @@
"gedcom.TITL": "Titolo", "gedcom.TITL": "Titolo",
"gedcom.WWW": "Sito web", "gedcom.WWW": "Sito web",
"gedcom._UPD": "Ultimo aggiornamento", "gedcom._UPD": "Ultimo aggiornamento",
"gedcom.birth": "Nome alla nascita",
"gedcom.married": "Nome da coniugato/a",
"gedcom.maiden": "Nome da nubile",
"gedcom.aka": "Conosciuto anche come",
"date.abt": "circa", "date.abt": "circa",
"date.cal": "calcolato", "date.cal": "calcolato",
"date.est": "stimato", "date.est": "stimato",

View File

@@ -58,6 +58,11 @@
"gedcom._UPD": "Ostatnia aktualizacja", "gedcom._UPD": "Ostatnia aktualizacja",
"gedcom.MARR": "Małżeństwo", "gedcom.MARR": "Małżeństwo",
"gedcom.DIV": "Rozwód", "gedcom.DIV": "Rozwód",
"gedcom.birth": "Nazwisko rodowe",
"gedcom.married": "Nazwisko po małżeństwie",
"gedcom.maiden": "Nazwisko panieńskie",
"gedcom.immigrant": "Nazwisko po imigracji",
"gedcom.aka": "Alias",
"date.abt": "około", "date.abt": "około",
"date.cal": "wyliczone", "date.cal": "wyliczone",
"date.est": "oszacowane", "date.est": "oszacowane",
@@ -85,5 +90,6 @@
"config.colors": "Kolory", "config.colors": "Kolory",
"config.colors.NO_COLOR": "brak", "config.colors.NO_COLOR": "brak",
"config.colors.COLOR_BY_GENERATION": "według pokolenia", "config.colors.COLOR_BY_GENERATION": "według pokolenia",
"config.colors.COLOR_BY_SEX": "według płci" "config.colors.COLOR_BY_SEX": "według płci",
"name.unknown_name": "N.N."
} }

View File

@@ -59,6 +59,10 @@
"gedcom.TITL": "Титул", "gedcom.TITL": "Титул",
"gedcom.WWW": "Веб-сайт WWW", "gedcom.WWW": "Веб-сайт WWW",
"gedcom._UPD": "Последнее обновление", "gedcom._UPD": "Последнее обновление",
"gedcom.birth": "Имя при рождении",
"gedcom.married": "Имя в браке",
"gedcom.maiden": "Девичья фамилия",
"gedcom.aka": "Он(а) же",
"date.abt": "около", "date.abt": "около",
"date.cal": "рассчитано", "date.cal": "рассчитано",
"date.est": "приблизительно", "date.est": "приблизительно",