File

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));
  }

}

results matching ""

    No results matching ""