src/app/home/timenet.service.ts
import { Injectable } from '@angular/core';
export interface TimeNetData {
fileName: string;
name: string;
gedVersion: string;
form: string;
charset: string;
persons: PersonData[];
relationships: RelationShipData[];
}
/**
* a point to draw the lines in the chart
*/
export interface GraphPoint {
x: number;
y: number;
}
export interface PersonData {
id: string;
sex: string;
dateOfBirth: number;
dateOfDeath: number;
name: string[];
baseLine: number;
doi: number;
parsedLine: GraphPoint[];
relevantRelationships: RelationShipData[];
block: LocationBlock;
}
/**
* a block to determine the position of it's contained persons
*/
export interface LocationBlock {
x: number;
y: number;
width: number;
height: number;
persons: PersonData[];
childBlocks: LocationBlock[];
}
export interface RelationShipData {
person1ID: string;
person2ID: string;
relationShipType: relationShipTypeEnum;
relationShipStartDate: number;
relationShipEndDate: number;
}
export enum relationShipTypeEnum {
'Spouse-Of',
'Child-Of',
'Sibbling-Of'
}
export interface FamilyData {
id: string;
spouseIDs: string[];
childIDs: string[];
}
@Injectable()
export class TimeNetDataService {
private static getMaxOfArray(numArray) {
return Math.max.apply(null, numArray);
}
private static getMinOfArray(numArray) {
return Math.min.apply(null, numArray);
}
private static cleanID(person1ID: string): string{
let pers = person1ID.split('@');
return pers[1];
}
public _familyData: FamilyData[] = [];
public _currentData: TimeNetData = {
fileName:'', name: '', gedVersion: '', form: '', charset: '', persons: [], relationships: []
};
private _marriageOffset: number = 20;
private _lifespanThreshold: number = 85;
private _cachedPersonBirth: string[] = [];
private _cachedPersonDeath: string[] = [];
public _averageAge: number = 85;
public _averageAgeBirth: number = 20;
private _showConsoleLog: boolean = true;
public newTimeNetData(fileName:string, name: string, gedVersion: string, form: string, charset: string): void {
this._currentData.name = name;
this._currentData.gedVersion = gedVersion;
this._currentData.form = form;
this._currentData.charset = charset;
this._currentData.persons = [];
this._currentData.relationships = [];
this._familyData = [];
this._currentData.fileName = fileName;
this._cachedPersonDeath = [];
this._cachedPersonBirth = [];
}
public getTimeNetData(): TimeNetData {
return this._currentData;
}
public addSpouseToFamily(familyID, spouseID)
{
if (familyID.includes('@')) {
familyID = TimeNetDataService.cleanID(familyID);
}
if (spouseID.includes('@')) {
spouseID = TimeNetDataService.cleanID(spouseID);
}
let fam = this._familyData.find((f) => f.id === familyID);
if (fam === undefined) {
this._familyData.push(<FamilyData>{id:familyID, spouseIDs: [], childIDs: []});
fam = this._familyData.find((f) => f.id === familyID);
}
fam.spouseIDs.push(spouseID);
}
public addChildToFamily(familyID, childID)
{
if (familyID.includes('@')) {
familyID = TimeNetDataService.cleanID(familyID);
}
if (childID.includes('@')) {
childID = TimeNetDataService.cleanID(childID);
}
var fam = this._familyData.find((f) => f.id === familyID);
if (fam === undefined) {
this._familyData.push(<FamilyData>{id:familyID, spouseIDs: [], childIDs: []});
fam = this._familyData.find((f) => f.id === familyID);
}
fam.childIDs.push(childID);
}
public addPerson(id: string, names: string[], sex: string, dateOfBirth: string, dateOfDeath: string){
let birthDate: number = TimeNetDataService.castStringToYear(dateOfBirth);
let deathDate: number = TimeNetDataService.castStringToYear(dateOfDeath);
if (id.includes('@')) {
id = TimeNetDataService.cleanID(id);
}
this._currentData.persons.push(
<PersonData> {id, name: names, sex, dateOfBirth: birthDate, dateOfDeath: deathDate, doi: 0.0});
}
public removePerson(person: PersonData): void {
let index = this._currentData.persons.indexOf(person, 0);
if (index > -1) {
this._currentData.persons.splice(index, 1);
}
}
public addReleationShip(person1ID: string, person2ID: string, relationShipType: relationShipTypeEnum,
relationShipStartDate: string, relationShipEndDate: string ): void {
if (person1ID.includes('@')) {
person1ID = TimeNetDataService.cleanID(person1ID);
}
if (person2ID.includes('@')) {
person2ID = TimeNetDataService.cleanID(person2ID);
}
let startDate: number = TimeNetDataService.castStringToYear(relationShipStartDate);
let endDate: number = TimeNetDataService.castStringToYear(relationShipEndDate);
this._currentData.relationships.push( <RelationShipData> {
person1ID,
person2ID,
relationShipType,
relationShipStartDate: startDate,
relationShipEndDate: endDate}
);
}
public removeRelationShip(rel: RelationShipData): void {
let index = this._currentData.relationships.indexOf(rel, 0);
if (index > -1) {
this._currentData.relationships.splice(index, 1);
}
}
public getPersonById(id: string): PersonData {
return this._currentData.persons.find((p) => p.id.substring(1, 11) === id.substring(1, 11));
}
private getAverageAgeOfPersons(): void {
let age = this._lifespanThreshold;
let lifeTimes: number[] = this._currentData.persons.filter((p) => isNaN(p.dateOfDeath) === false
&& isNaN(p.dateOfBirth) === false).map((p) => p.dateOfDeath - p.dateOfBirth);
if( lifeTimes.length > 0) {
this._averageAge = Math.round(lifeTimes.reduce((a, b) => (a + b)) / lifeTimes.length);
} else {
this._averageAge = age;
}
}
private getAverageAgeOfPregnancy(): void {
let age = this._marriageOffset;
let totalYears = 0;
let numberOfChildren = 0;
for (let rel of this._currentData.relationships
.filter((r) => r.relationShipType === relationShipTypeEnum['Child-Of']))
{
let child = this.getPersonById(rel.person1ID);
let parent = this.getPersonById(rel.person2ID);
if ((isNaN(child.dateOfBirth) === false) && (isNaN(parent.dateOfBirth) === false)) {
totalYears += child.dateOfBirth - parent.dateOfBirth;
numberOfChildren++;
}
}
if (totalYears > 0) {
age = Math.round(totalYears / numberOfChildren);
}
this._averageAgeBirth = age;
}
// final tasks which are made when the whole gedcom file information is read
public finishImport(): void {
console.log('Reading finished, starting missing data estimation');
// estimate statistics
this.getAverageAgeOfPersons();
this.getAverageAgeOfPregnancy();
if(this._currentData.persons.length > 1000) {
this._showConsoleLog = false;
}
// check persondata, convert to timestamp
this.checkPersonData();
// check relationships for duplicates
this.checkRelationshipData();
// missing data estimation
this.missingDataEstimation();
// find relationships of each person for optimization
this.findFittingRelationships();
}
private findFittingRelationships(): void {
for (let person of this._currentData.persons) {
person.relevantRelationships =
this._currentData.relationships.filter((r) => r.person1ID === person.id);
person.relevantRelationships = person.relevantRelationships.concat(
this._currentData.relationships.filter((r) => r.person2ID === person.id));
for (var i = 0; i < person.relevantRelationships.length; i++) {
for (var j = i + 1; j < person.relevantRelationships.length; j++) {
let rel1 = person.relevantRelationships[i];
let rel2 = person.relevantRelationships[j];
if (rel1.relationShipType === relationShipTypeEnum['Spouse-Of']
&& rel2.relationShipType === relationShipTypeEnum['Spouse-Of']
&& rel1.relationShipStartDate === rel2.relationShipStartDate
&& rel1.relationShipEndDate === rel2.relationShipEndDate
&& (rel1.person1ID === rel2.person1ID
|| rel1.person2ID === rel2.person2ID
|| rel1.person1ID === rel2.person2ID
|| rel1.person2ID === rel2.person1ID)) {
person.relevantRelationships.splice(j, 1);
}
}
}
}
}
private missingDataEstimation(): void {
if ( this._currentData.persons.filter((p) => isNaN(p.dateOfBirth) === false).length === 0) {
this._currentData.persons[0].dateOfBirth = 0;
}
// check for Birth date first run
for (let person of this._currentData.persons.filter((p) => isNaN(p.dateOfBirth) === true)) {
person.dateOfBirth = this.estimateBirthDate(person.id);
}
// run the missing people again
let oldCachedPerson: string[] = [];
let newCachedPerson: string[] = this._cachedPersonBirth.slice();
while (oldCachedPerson.length !== newCachedPerson.length) {
oldCachedPerson = newCachedPerson.slice();
if (oldCachedPerson.length > 0) {
newCachedPerson = [];
for ( let i = 0; i < oldCachedPerson.length; i++) {
let persID: string = oldCachedPerson[i];
let birth = this.estimateBirthDate(persID);
if (isNaN(birth) === false) {
this._currentData.persons.find((p) => p.id === persID).dateOfBirth = birth;
} else {
newCachedPerson.push(persID);
}
}
}
}
// No information for those people available
for (let persID of newCachedPerson) {
this._currentData.persons.find((p) => p.id === persID).dateOfBirth = this.getEarliestYear();
console.log('No data available, set birth to earliest date for ' + persID);
}
if ( this._currentData.persons.filter((p) => isNaN(p.dateOfDeath) === false).length === 0) {
this._currentData.persons[0].dateOfDeath = this._averageAge;
}
// check for death date first run
for (let person of this._currentData.persons.filter((p) => isNaN(p.dateOfDeath) === true)) {
// check if its probability that person is alive
if (person.dateOfBirth + this._averageAge < this.getLatestYear()) {
person.dateOfDeath = this.estimateDeathDate(person.id);
}else {
// set to latest year
console.log('Estimated person is alive therefore set to latest year: ' + person.id);
person.dateOfDeath = this.getLatestYear();
}
}
// run the missing people again
oldCachedPerson = [];
newCachedPerson = this._cachedPersonDeath.slice();
while (oldCachedPerson.length !== newCachedPerson.length) {
oldCachedPerson = newCachedPerson.slice();
if (oldCachedPerson.length > 0) {
newCachedPerson = [];
for (let i = 0; i < oldCachedPerson.length; i++) {
let persID: string = oldCachedPerson[i];
let birth = this.estimateDeathDate(persID);
if (isNaN(birth) === false) {
this._currentData.persons.find((p) => p.id === persID).dateOfDeath = birth;
} else {
newCachedPerson.push(persID);
}
}
}
}
// No information for those people available
for (let persID of newCachedPerson) {
this._currentData.persons.find((p) => p.id === persID).dateOfDeath
= this._currentData.persons.find((p) => p.id === persID).dateOfBirth + this._averageAge ;
console.log('No data available, set death to average lifetime for ' + persID);
}
// Missing data of marriages
for (let rel of this._currentData.relationships.filter((r) => r.relationShipType === 0)) {
rel.relationShipStartDate = Math.round(
(this.getPersonById(rel.person1ID).dateOfBirth + this.getPersonById(rel.person2ID).dateOfBirth) / 2) + this._averageAgeBirth;
/*
if (this.getPersonById(rel.person1ID).dateOfDeath > this.getPersonById(rel.person2ID).dateOfDeath) {
rel.relationShipEndDate = this.getPersonById(rel.person2ID).dateOfDeath;
} else {
rel.relationShipEndDate = this.getPersonById(rel.person1ID).dateOfDeath;
}*/
}
}
private checkRelationshipData(): void {
// convert families to relationships
for (let i = 0; i < this._familyData.length; i++) {
let famData = this._familyData[i];
// remove double entries
famData.childIDs = Array.from(new Set(famData.childIDs));
famData.spouseIDs = Array.from(new Set(famData.spouseIDs));
// Add relationships child of and sibbling of
for (let child of famData.childIDs) {
for (let parent of famData.spouseIDs) {
this.addReleationShip(child, parent, 1, '', '');
}
for (let sibbling of famData.childIDs) {
if (sibbling !== child) {
this.addReleationShip(child, sibbling, 2, '', '');
}
}
}
for (let parent of famData.spouseIDs) {
for (let spouse of famData.spouseIDs) {
if (parent !== spouse) {
this.addReleationShip(parent, spouse, 0, '', '');
}
}
}
}
}
private checkPersonData(): void {
}
private static castStringToYear(dateString: string): number {
let date: number;
// save it with date null, missing values estimation is done
// when the whole data information is available.
if (dateString === null || dateString === '') {
date = NaN;
}else {
let yearOffset: number = 0;
// check if date has lotr ages
if (dateString.includes('FA')) {
dateString = dateString.replace('FA', '');
yearOffset = 0;
} else if (dateString.includes('SA')) {
dateString = dateString.replace('SA', '');
yearOffset = 583;
} else if (dateString.includes('TA')) {
dateString = dateString.replace('TA', '');
yearOffset = 583 + 3441;
} else if (dateString.includes('FoA')) {
dateString = dateString.replace('FoA', '');
yearOffset = 583 + 3441 + 3021;
}else if (dateString.includes('SR')) {
dateString = dateString.replace('SR', '');
yearOffset = 1600 + 583 + 3441;
}
if ( dateString.toLowerCase().startsWith('bef') || dateString.toLowerCase().startsWith('aft')
|| dateString.toLowerCase().startsWith('est') || dateString.toLowerCase().startsWith('abt')) {
dateString = dateString.slice(4);
}
// use date parser for different date formats
let tempdate = new Date(dateString);
// workaround for dates under 100, because the date parser converts them to 19xx
if (dateString.length <= 4) {
date = parseInt(dateString) + yearOffset;
} else {
date = tempdate.getFullYear() + yearOffset;
}
// when not inthe yearof the sun set it to 0
if (dateString.includes('YT')) {
date = 0;
}
}
return date;
}
private estimateDeathDate(personID: string): number {
// get all relationship for a person
let relations = this._currentData.relationships.filter((r) => r.person1ID === personID
|| r.person2ID === personID);
// mean sibling death
let sibblings: string[] = relations
.filter((r) => r.person1ID === personID && r.relationShipType === 2)
.map((r) => r.person2ID);
let yearsOfSibs = 0;
let numberOfSibsWithDeath = 0;
for (let sib of sibblings) {
if (isNaN(this.getPersonById(sib).dateOfDeath) === false) {
yearsOfSibs += this.getPersonById(sib).dateOfDeath;
numberOfSibsWithDeath++;
}
}
if (yearsOfSibs > 0) {
if(this._showConsoleLog) {
console.log('Estimated Death(sibbling average) for ' + personID);
}
return Math.round(yearsOfSibs / numberOfSibsWithDeath);
}
// mean spouse death
let spouses: string[] = relations
.filter((r) => r.person1ID === personID && r.relationShipType === 0)
.map((r) => r.person2ID);
let yearsOfSpouse = 0;
let numberOfSpouseWithDeath = 0;
for (let spouse of spouses) {
if (isNaN(this.getPersonById(spouse).dateOfDeath) === false) {
yearsOfSpouse += this.getPersonById(spouse).dateOfDeath;
numberOfSpouseWithDeath++;
}
}
if (yearsOfSpouse > 0) {
if(this._showConsoleLog) {
console.log('Estimated Death(spouse average) for ' + personID);
}
return Math.round(yearsOfSpouse / numberOfSpouseWithDeath);
}
let childs: string[] = relations
.filter((r) => r.person2ID === personID && r.relationShipType === 1)
.map((r) => r.person1ID);
let yearsOfChildren = 0;
let numberOfChildsWithDeath = 0;
for (let child of childs) {
if (isNaN(this.getPersonById(child).dateOfDeath) === false) {
yearsOfChildren += this.getPersonById(child).dateOfDeath;
numberOfChildsWithDeath++;
}
}
if (yearsOfChildren > 0) {
if(this._showConsoleLog) {
console.log('Estimated Death(children average) for ' + personID);
}
return Math.round(yearsOfChildren / numberOfChildsWithDeath) - this._averageAgeBirth;
}
this._cachedPersonDeath.push(personID);
return NaN;
}
private estimateBirthDate(personID: string): number {
// get all relationship for a person
let relations = this._currentData.relationships.filter((r) => r.person1ID === personID
|| r.person2ID === personID);
// check for parent marriage
// actually no use because, marriage data is not saved in gedcom files...
let parentsRel = relations.filter((r) => r.person1ID === personID && r.relationShipType === 1);
/*if (parentsRel.length === 2) {
let parent1 = parentsRel[0].person1ID;
let parent2 = parentsRel[1].person2ID;
let mariage = this._currentData.relationships
.filter((r) => (r.person1ID === parent1 && r.person2ID === parent2) ||
(r.person1ID === parent2 && r.person2ID === parent1));
for (let obj of mariage) {
if ( isNaN(obj.relationShipStartDate) === false) {
return obj.relationShipStartDate;
}
}
}*/
// mean sibling birth
let sibblings: string[] = relations
.filter((r) => r.person1ID === personID && r.relationShipType === 2)
.map((r) => r.person2ID);
let yearsOfSibs = 0;
let numberOfSibsWithBirth = 0;
for (let sib of sibblings) {
if (isNaN(this.getPersonById(sib).dateOfBirth) === false) {
yearsOfSibs += this.getPersonById(sib).dateOfBirth;
numberOfSibsWithBirth++;
}
}
if (yearsOfSibs > 0) {
if(this._showConsoleLog) {
console.log('Estimated Birth(sibbling) for ' + personID);
}
return Math.round(yearsOfSibs / numberOfSibsWithBirth);
}
// mean spouse birth
let spouses: string[] = relations
.filter((r) => r.person1ID === personID && r.relationShipType === 0)
.map((r) => r.person2ID);
let yearsOfSpouse = 0;
let numberOfSpouseWithBirth = 0;
for (let spouse of spouses) {
if (isNaN(this.getPersonById(spouse).dateOfBirth) === false) {
yearsOfSpouse += this.getPersonById(spouse).dateOfBirth;
numberOfSpouseWithBirth++;
}
}
if (yearsOfSpouse > 0) {
if(this._showConsoleLog) {
console.log('Estimated Birth(spouse) for ' + personID);
}
return Math.round(yearsOfSpouse / numberOfSpouseWithBirth);
}
// estimate if death is known
let pers = this.getPersonById(personID);
if (isNaN(pers.dateOfDeath) === false) {
if(this._showConsoleLog) {
console.log('Estimated Birth(death+average) for ' + personID);
}
return pers.dateOfDeath - this._averageAge;
}
// estimate if birth of parents is known
let parIds = parentsRel.map((p) => p.person2ID);
for (let parid of parIds) {
if (isNaN(this.getPersonById(parid).dateOfBirth) === false) {
if(this._showConsoleLog) {
console.log('Estimated Birth(parent birth + average) for' + personID);
}
return this.getPersonById(parid).dateOfBirth + this._averageAgeBirth;
}
}
// estimate if birth of children is known
let childs = relations
.filter((r) => r.person2ID === personID && r.relationShipType === 1)
.map((p) => p.person1ID);
let childBirthYears: number[] = [];
for (let childid of childs) {
if (isNaN(this.getPersonById(childid).dateOfBirth) === false) {
childBirthYears.push(this.getPersonById(childid).dateOfBirth);
}
}
if (childBirthYears.length > 0) {
let yearOfOldestChild = TimeNetDataService.getMinOfArray(childBirthYears);
if (this._showConsoleLog) {
console.log('Estimated Birth(child birth + average) for' + personID);
}
return yearOfOldestChild - this._averageAgeBirth;
}
this._cachedPersonBirth.push(personID);
return NaN;
}
private getEarliestYear(): number {
return TimeNetDataService.getMinOfArray(this._currentData.persons
.filter((p) => isNaN(p.dateOfBirth) === false).map((r) => r.dateOfBirth));
}
private getLatestYear(): number {
return TimeNetDataService.getMaxOfArray(this._currentData.persons
.filter((p) => isNaN(p.dateOfDeath) === false).map((r) => r.dateOfDeath));
}
}