// myAnalyses provides functions to manage genome analyses for People and Couples // Analyses are created using the createGeneticAnalysis package package myAnalyses import "seekia/internal/helpers" import "seekia/internal/appMemory" import "seekia/internal/appValues" import "seekia/internal/localFilesystem" import "seekia/internal/myDatastores/myMapList" import "seekia/internal/genetics/createGeneticAnalysis" import "seekia/internal/genetics/prepareRawGenomes" import "seekia/internal/genetics/readGeneticAnalysis" import "seekia/internal/genetics/myGenomes" import "path/filepath" import "strings" import "time" import "sync" import "errors" // This will be locked anytime analyses are being added/deleted to the myMapList datastores var updatingMyAnalysesMutex sync.Mutex var myPersonAnalysesMapListDatastore *myMapList.MyMapList var myCoupleAnalysesMapListDatastore *myMapList.MyMapList // This function must be called whenever an app user signs in func InitializeMyAnalysesDatastores()error{ updatingMyAnalysesMutex.Lock() defer updatingMyAnalysesMutex.Unlock() newMyPersonAnalysesMapListDatastore, err := myMapList.CreateNewMapList("MyPersonAnalyses") if (err != nil) { return err } newMyCoupleAnalysesMapListDatastore, err := myMapList.CreateNewMapList("MyCoupleAnalyses") if (err != nil) { return err } myPersonAnalysesMapListDatastore = newMyPersonAnalysesMapListDatastore myCoupleAnalysesMapListDatastore = newMyCoupleAnalysesMapListDatastore return nil } // This function must be called whenever an app user signs in func CreateMyAnalysesFolder() error{ userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } myAnalysesFolderPath := filepath.Join(userDirectory, "MyAnalyses") _, err = localFilesystem.CreateFolder(myAnalysesFolderPath) if (err != nil) { return err } return nil } // This function should be called whenever an app user signs in func PruneOldAnalyses()error{ updatingMyAnalysesMutex.Lock() defer updatingMyAnalysesMutex.Unlock() //TODO: This function should delete analyses we don't need anymore // These include analyses for people who dont exist anymore, and analyses for people/couples whom have newer analyses return nil } func DeleteAllAnalysesForPerson(personIdentifier string)error{ updatingMyAnalysesMutex.Lock() defer updatingMyAnalysesMutex.Unlock() mapToDelete := map[string]string{ "PersonIdentifer": personIdentifier, } err := myPersonAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) if (err != nil) { return err } mapToDelete = map[string]string{ "PersonAIdentifier": personIdentifier, } err = myCoupleAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) if (err != nil) { return err } mapToDelete = map[string]string{ "PersonBIdentifier": personIdentifier, } err = myCoupleAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) if (err != nil) { return err } return nil } func DeleteAllAnalysesForCouple(inputPersonAIdentifier string, inputPersonBIdentifier string)error{ updatingMyAnalysesMutex.Lock() defer updatingMyAnalysesMutex.Unlock() personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) if (err != nil) { return err } mapToDelete := map[string]string{ "PersonAIdentifier": personAIdentifier, "PersonBIdentifier": personBIdentifier, } err = myCoupleAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) if (err != nil) { return err } return nil } // This function is used to sort the person identifiers, so that each pair of identifiers will only map to a single couple // PersonA and PersonB will always follow this order within a couple analysis // The sorting method has no significance func GetPeopleIdentifiersSortedForCouple(personAIdentifier string, personBIdentifier string)(string, string, error){ if (personAIdentifier == personBIdentifier){ return "", "", errors.New("GetPeopleIdentifiersSortedForCouple called with identical person identifiers: " + personAIdentifier) } if (personAIdentifier < personBIdentifier){ return personAIdentifier, personBIdentifier, nil } return personBIdentifier, personAIdentifier, nil } //Outputs: // -bool: Any analysis exists // -string: Newest analysis identifier // -int64: Time newest analysis was performed // -[]string: List of genomes analyzed // -bool: Newer analysis version available // -This is not the same as new genomes being available. New version can be for the same genomes. // -To fully determine if the analysis is up to date, we must check if new (or less) genomes are available to analyse for the person // -error func GetPersonNewestGeneticAnalysisInfo(personIdentifier string)(bool, string, int64, []string, bool, error){ lookupMap := map[string]string{ "PersonIdentifier": personIdentifier, } anyItemsFound, analysisItemsMapList, err := myPersonAnalysesMapListDatastore.GetMapListItems(lookupMap) if (err != nil) { return false, "", 0, nil, false, err } if (anyItemsFound == false){ return false, "", 0, nil, false, nil } // Below is the newest analysis version, which would update if the user had updated their Seekia client appAnalysisVersion := appValues.GetGeneticAnalysisVersion() newestAnalysisFound := false newestAnalysisCreatedTime := int64(0) newestAnalysisIdentifier := "" newestAnalysisAnalyzedGenomesList := []string{} newerAnalysisVersionAvailable := false for _, analysisMap := range analysisItemsMapList{ currentPersonIdentifier, exists := analysisMap["PersonIdentifier"] if (exists == false) { return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonIdentifier") } if (personIdentifier != currentPersonIdentifier){ return false, "", 0, nil, false, errors.New("GetMapListItems returning map with different personIdentifier.") } timeCreated, exists := analysisMap["TimeCreated"] if (exists == false) { return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing TimeCreated") } timeCreatedInt64, err := helpers.ConvertStringToInt64(timeCreated) if (err != nil) { return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid timeCreated: " + timeCreated) } if (newestAnalysisFound == true && newestAnalysisCreatedTime > timeCreatedInt64){ continue } newestAnalysisFound = true analysisIdentifier, exists := analysisMap["AnalysisIdentifier"] if (exists == false) { return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisIdentifier") } analyzedGenomes, exists := analysisMap["AnalyzedGenomes"] if (exists == false) { return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalyzedGenomes") } analyzedGenomesList := strings.Split(analyzedGenomes, "+") analysisVersion, exists := analysisMap["AnalysisVersion"] if (exists == false) { return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisVersion") } analysisVersionInt, err := helpers.ConvertStringToInt(analysisVersion) if (err != nil) { return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid AnalysisVersion: " + analysisVersion) } if (appAnalysisVersion > analysisVersionInt){ // This analysis is not of the newest analysis version // The user should run a new analysis on the same genomes to get the newest, most up to date and informative results newerAnalysisVersionAvailable = true } else { newerAnalysisVersionAvailable = false } newestAnalysisCreatedTime = timeCreatedInt64 newestAnalysisIdentifier = analysisIdentifier newestAnalysisAnalyzedGenomesList = analyzedGenomesList } if (newestAnalysisFound == false){ return false, "", 0, nil, false, errors.New("GetMapListItems not returning any items when anyItemsFound == true.") } return true, newestAnalysisIdentifier, newestAnalysisCreatedTime, newestAnalysisAnalyzedGenomesList, newerAnalysisVersionAvailable, nil } //Outputs: // -bool: Analysis exists // -string: Newest analysis identifier // -int64: Time newest analysis was performed // -[]string: Person A list of genomes analyzed // -[]string: Person B list of genomes analyzed // -bool: Newer analysis version available // -This is not the same as new genomes being available. New version can be for the same genomes. // -To fully determine if the analysis is up to date, we must check if new (or less) genomes are available to analyse for the person // -error func GetCoupleNewestGeneticAnalysisInfo(inputPersonAIdentifier string, inputPersonBIdentifier string)(bool, string, int64, []string, []string, bool, error){ personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) if (err != nil) { return false, "", 0, nil, nil, false, err } lookupMap := map[string]string{ "PersonAIdentifier": personAIdentifier, "PersonBIdentifier": personBIdentifier, } anyItemsFound, analysisItemsMapList, err := myCoupleAnalysesMapListDatastore.GetMapListItems(lookupMap) if (err != nil) { return false, "", 0, nil, nil, false, err } if (anyItemsFound == false){ return false, "", 0, nil, nil, false, nil } // Below is the newest analysis version, which would update if the user had updated their Seekia client appAnalysisVersion := appValues.GetGeneticAnalysisVersion() newestAnalysisFound := false newestAnalysisCreatedTime := int64(0) newestAnalysisIdentifier := "" newestAnalysisPersonAAnalyzedGenomesList := []string{} newestAnalysisPersonBAnalyzedGenomesList := []string{} newerAnalysisVersionAvailable := false for _, analysisMap := range analysisItemsMapList{ currentPersonAIdentifier, exists := analysisMap["PersonAIdentifier"] if (exists == false) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonAIdentifier") } if (personAIdentifier != currentPersonAIdentifier){ return false, "", 0, nil, nil, false, errors.New("GetMapListItems returning map with different personAIdentifier.") } currentPersonBIdentifier, exists := analysisMap["PersonBIdentifier"] if (exists == false) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonBIdentifier") } if (personBIdentifier != currentPersonBIdentifier){ return false, "", 0, nil, nil, false, errors.New("GetMapListItems returning map with different personBIdentifier.") } timeCreated, exists := analysisMap["TimeCreated"] if (exists == false) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing TimeCreated") } timeCreatedInt64, err := helpers.ConvertStringToInt64(timeCreated) if (err != nil) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid timeCreated: " + timeCreated) } if (newestAnalysisFound == true && newestAnalysisCreatedTime > timeCreatedInt64){ continue } newestAnalysisFound = true analysisIdentifier, exists := analysisMap["AnalysisIdentifier"] if (exists == false) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisIdentifier") } personAAnalyzedGenomes, exists := analysisMap["PersonAAnalyzedGenomes"] if (exists == false) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonAAnalyzedGenomes") } personAAnalyzedGenomesList := strings.Split(personAAnalyzedGenomes, "+") personBAnalyzedGenomes, exists := analysisMap["PersonBAnalyzedGenomes"] if (exists == false) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonBAnalyzedGenomes") } personBAnalyzedGenomesList := strings.Split(personBAnalyzedGenomes, "+") analysisVersion, exists := analysisMap["AnalysisVersion"] if (exists == false) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisVersion") } analysisVersionInt, err := helpers.ConvertStringToInt(analysisVersion) if (err != nil) { return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid AnalysisVersion: " + analysisVersion) } if (appAnalysisVersion > analysisVersionInt){ // This analysis is not of the newest analysis version // The user should run a new analysis on the same genomes to get the newest, most up to date and informative results newerAnalysisVersionAvailable = true } else { newerAnalysisVersionAvailable = false } newestAnalysisCreatedTime = timeCreatedInt64 newestAnalysisIdentifier = analysisIdentifier newestAnalysisPersonAAnalyzedGenomesList = personAAnalyzedGenomesList newestAnalysisPersonBAnalyzedGenomesList = personBAnalyzedGenomesList } if (newestAnalysisFound == false){ return false, "", 0, nil, nil, false, errors.New("GetMapListItems not returning any items when anyItemsFound == true.") } // We have to return the personA/PersonB analyzed genomes list in the same order that they came in if (inputPersonAIdentifier == personAIdentifier){ // No swapping happened. return true, newestAnalysisIdentifier, newestAnalysisCreatedTime, newestAnalysisPersonAAnalyzedGenomesList, newestAnalysisPersonBAnalyzedGenomesList, newerAnalysisVersionAvailable, nil } // We swap the personA/PersonB analyzed genomes lists return true, newestAnalysisIdentifier, newestAnalysisCreatedTime, newestAnalysisPersonBAnalyzedGenomesList, newestAnalysisPersonAAnalyzedGenomesList, newerAnalysisVersionAvailable, nil } // This function should only be used to retrieve an existing analysis // It can be used for person and couple genetic analyses //Outputs: // -bool: Analysis found // -[]map[string]string: Analysis map list // -error func GetGeneticAnalysis(analysisIdentifier string)(bool, []map[string]string, error){ userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return false, nil, err } analysisFileName := analysisIdentifier + ".json" analysisFilePath := filepath.Join(userDirectory, "MyAnalyses", analysisFileName) fileExists, fileBytes, err := localFilesystem.GetFileContents(analysisFilePath) if (err != nil) { return false, nil, err } if (fileExists == false){ return false, nil, nil } fileString := string(fileBytes) analysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(fileString) if (err != nil) { return false, nil, err } return true, analysisMapList, nil } // This map keeps track of current person analyses being generated // Map structure: Person identifier -> Process identifier var personAnalysisProcessesMap map[string]string = make(map[string]string) var personAnalysisProcessesMapMutex sync.RWMutex // This map keeps track of current couple analyses being generated // Map structure: PersonAIdentifier + "+" + PersonBIdentifier -> Process identifier var coupleAnalysisProcessesMap map[string]string = make(map[string]string) var coupleAnalysisProcessesMapMutex sync.RWMutex // Returns the process identifier for the current running genetic analyses for the person // Outputs: // -bool: Any process found (The process may be complete) // -string: Process identifier // -error func GetPersonGeneticAnalysisProcessIdentifier(personIdentifier string)(bool, string, error){ personAnalysisProcessesMapMutex.RLock() processIdentifier, exists := personAnalysisProcessesMap[personIdentifier] personAnalysisProcessesMapMutex.RUnlock() if (exists == false){ return false, "", nil } return true, processIdentifier, nil } // Returns the process identifier for the current running genetic analyses for the couple // Outputs: // -bool: Any process found (The process may be complete) // -string: Process identifier // -error func GetCoupleGeneticAnalysisProcessIdentifier(inputPersonAIdentifier string, inputPersonBIdentifier string)(bool, string, error){ personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) if (err != nil) { return false, "", err } coupleIdentifier := personAIdentifier + "+" + personBIdentifier coupleAnalysisProcessesMapMutex.RLock() processIdentifier, exists := coupleAnalysisProcessesMap[coupleIdentifier] coupleAnalysisProcessesMapMutex.RUnlock() if (exists == false){ return false, "", nil } return true, processIdentifier, nil } //Outputs: // -bool: Process found // -bool: Process is complete // -bool: Process encountered error // -error: Error encountered by process // -int: Process percentage complete // -error func GetAnalysisProcessInfo(processIdentifier string)(bool, bool, bool, error, int, error){ processExists, processPercentageComplete := appMemory.GetMemoryEntry(processIdentifier + "_ProgressPercentageComplete") if (processExists == false){ // This should not happen... return false, false, false, nil, 0, nil } exists, errorEncountered := appMemory.GetMemoryEntry(processIdentifier + "_ErrorEncountered") if (exists == true){ errorEncounteredError := errors.New(errorEncountered) return true, true, true, errorEncounteredError, 100, nil } if (processPercentageComplete == "100"){ return true, true, false, nil, 100, nil } processPercentageCompleteInt, err := helpers.ConvertStringToInt(processPercentageComplete) if (err != nil) { return false, false, false, nil, 0, errors.New("Invalid processPercentageComplete for genetic analysis process: " + processPercentageComplete) } if (processPercentageCompleteInt < 0 || processPercentageCompleteInt > 100){ return false, false, false, nil, 0, errors.New("Invalid processPercentageComplete for genetic analysis process: " + processPercentageComplete) } return true, false, false, nil, processPercentageCompleteInt, nil } func StopProcess(processIdentifier string){ appMemory.SetMemoryEntry(processIdentifier + "_StopProcess", "Yes") appMemory.SetMemoryEntry(processIdentifier + "_ProgressPercentageComplete", "100") } // This funciton will stop any currently running processes for this person and start generating a new genetic analysis //Outputs: // -string: New process identifier // -error func StartCreateNewPersonGeneticAnalysis(personIdentifier string)(string, error){ personAnalysisProcessesMapMutex.Lock() existingProcessIdentifier, exists := personAnalysisProcessesMap[personIdentifier] if (exists == true){ progressExists, processPercentageComplete := appMemory.GetMemoryEntry(existingProcessIdentifier + "_ProgressPercentageComplete") if (progressExists == false){ personAnalysisProcessesMapMutex.Unlock() return "", errors.New("personAnalysisProcessesMap process percentageComplete value not found.") } if (processPercentageComplete != "100"){ // There is another process running. // We will stop the current process for this person StopProcess(existingProcessIdentifier) } } newProcessIdentifier, err := helpers.GetNewRandomHexString(18) if (err != nil) { return "", err } personAnalysisProcessesMap[personIdentifier] = newProcessIdentifier personAnalysisProcessesMapMutex.Unlock() updatePercentageCompleteFunction := func(newPercentage int)error{ if (newPercentage < 0 || newPercentage > 100){ return errors.New("Invalid person analysis generation progress percentage.") } newPercentageString := helpers.ConvertIntToString(newPercentage) appMemory.SetMemoryEntry(newProcessIdentifier + "_ProgressPercentageComplete", newPercentageString) return nil } err = updatePercentageCompleteFunction(0) if (err != nil) { return "", err } createNewPersonGeneticAnalysis := func()error{ checkIfProcessIsStopped := func()bool{ exists, _ := appMemory.GetMemoryEntry(newProcessIdentifier + "_StopProcess") if (exists == true){ return true } return false } personGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personIdentifier) if (err != nil){ return err } if (len(personGenomesMapList) == 0){ return errors.New("Cannot create person genetic analysis: No genomes found for Person.") } genomeIdentifiersList := make([]string, 0, len(personGenomesMapList)) genomesList := make([]prepareRawGenomes.RawGenomeWithMetadata, 0, len(personGenomesMapList)) finalIndex := len(personGenomesMapList) - 1 for index, genomeMap := range personGenomesMapList{ newPercentageProgress, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 0, 10) if (err != nil) { return err } err = updatePercentageCompleteFunction(newPercentageProgress) if (err != nil) { return err } currentPersonIdentifier, exists := genomeMap["PersonIdentifier"] if (exists == false) { return errors.New("PersonGenomesMapList malformed: Item missing PersonIdentifier") } if (personIdentifier != currentPersonIdentifier){ return errors.New("GetAllPersonGenomesMapList returning genome for different person.") } genomeIdentifier, exists := genomeMap["GenomeIdentifier"] if (exists == false) { return errors.New("PersonGenomesMapList malformed: Item missing GenomeIdentifier") } genomeRawDataString, err := myGenomes.GetGenomeRawDataString(genomeIdentifier) if (err != nil) { return err } rawGenomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, genomeRawDataString) if (err != nil) { return err } if (rawGenomeIsValid == false){ return errors.New("myGenomes contains invalid rawGenomeDataString: " + genomeIdentifier) } genomesList = append(genomesList, rawGenomeWithMetadata) genomeIdentifiersList = append(genomeIdentifiersList, genomeIdentifier) } analysisUpdatePercentageCompleteFunction := func(inputProgress int)error{ newPercentageProgress, err := helpers.ScaleNumberProportionally(true, inputProgress, 0, 100, 10, 10) if (err != nil) { return err } err = updatePercentageCompleteFunction(newPercentageProgress) if (err != nil) { return err } return nil } processCompleted, newGeneticAnalysisString, err := createGeneticAnalysis.CreatePersonGeneticAnalysis(genomesList, analysisUpdatePercentageCompleteFunction, checkIfProcessIsStopped) if (err != nil) { return err } if (processCompleted == false){ // User stopped the analysis mid-way return nil } analyzedGenomesListString := strings.Join(genomeIdentifiersList, "+") analysisIdentifier, err := helpers.GetNewRandomHexString(17) if (err != nil) { return err } currentTime := time.Now().Unix() currentTimeString := helpers.ConvertInt64ToString(currentTime) currentAnalysisVersion := appValues.GetGeneticAnalysisVersion() currentAnalysisVersionString := helpers.ConvertIntToString(currentAnalysisVersion) newAnalysisMap := map[string]string{ "PersonIdentifier": personIdentifier, "AnalysisIdentifier": analysisIdentifier, "TimeCreated": currentTimeString, "AnalyzedGenomes": analyzedGenomesListString, "AnalysisVersion": currentAnalysisVersionString, } updatingMyAnalysesMutex.Lock() defer updatingMyAnalysesMutex.Unlock() err = myPersonAnalysesMapListDatastore.AddMapListItem(newAnalysisMap) if (err != nil) { return err } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } myAnalysesFolderPath := filepath.Join(userDirectory, "MyAnalyses") analysisFileName := analysisIdentifier + ".json" err = localFilesystem.CreateOrOverwriteFile([]byte(newGeneticAnalysisString), myAnalysesFolderPath, analysisFileName) if (err != nil) { return err } err = updatePercentageCompleteFunction(100) if (err != nil) { return err } personAnalysisProcessesMapMutex.Lock() delete(personAnalysisProcessesMap, personIdentifier) personAnalysisProcessesMapMutex.Unlock() return nil } runAnalysis := func(){ err := createNewPersonGeneticAnalysis() if (err != nil){ appMemory.SetMemoryEntry(newProcessIdentifier + "_ProgressPercentageComplete", "100") appMemory.SetMemoryEntry(newProcessIdentifier + "_ErrorEncountered", err.Error()) return } } go runAnalysis() return newProcessIdentifier, nil } // This will stop any currently running processes for this couple and start generating a new genetic analysis //Outputs: // -string: New process identifier // -error func StartCreateNewCoupleGeneticAnalysis(inputPersonAIdentifier string, inputPersonBIdentifier string)(string, error){ personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) if (err != nil) { return "", err } coupleIdentifier := personAIdentifier + "+" + personBIdentifier coupleAnalysisProcessesMapMutex.Lock() existingProcessIdentifier, exists := coupleAnalysisProcessesMap[coupleIdentifier] if (exists == true){ progressExists, processPercentageComplete := appMemory.GetMemoryEntry(existingProcessIdentifier + "_ProgressPercentageComplete") if (progressExists == false){ coupleAnalysisProcessesMapMutex.Unlock() return "", errors.New("coupleAnalysisProcessesMap process percentageComplete value not found.") } if (processPercentageComplete != "100"){ // There is another process running. // We will stop the current process for this person StopProcess(existingProcessIdentifier) } } newCoupleProcessIdentifier, err := helpers.GetNewRandomHexString(18) if (err != nil) { return "", err } coupleAnalysisProcessesMap[coupleIdentifier] = newCoupleProcessIdentifier coupleAnalysisProcessesMapMutex.Unlock() updatePercentageCompleteFunction := func(newPercentage int)error{ if (newPercentage < 0 || newPercentage > 100){ return errors.New("Invalid couple analysis generation progress percentage.") } newPercentageString := helpers.ConvertIntToString(newPercentage) appMemory.SetMemoryEntry(newCoupleProcessIdentifier + "_ProgressPercentageComplete", newPercentageString) return nil } err = updatePercentageCompleteFunction(0) if (err != nil) { return "", err } createNewCoupleGeneticAnalysis := func()error{ checkIfProcessIsStopped := func()bool{ exists, _ := appMemory.GetMemoryEntry(newCoupleProcessIdentifier + "_StopProcess") if (exists == true){ return true } return false } // We need both personA and personB to have an analysis before performing the couple analysis // We will see if an analysis is already running for each person // We will either start a new analysis for each person or monitor the existing one until it is done personIdentifiersList := []string{personAIdentifier, personBIdentifier} for index, personIdentifier := range personIdentifiersList{ // This function will return the percentage progress that this person's analysis will use of the entire couple analysis getPercentageRangeForPersonAnalysis := func()(int, int){ if (index == 0){ return 0, 33 } return 33, 66 } personPercentageRangeStart, personPercentageRangeEnd := getPercentageRangeForPersonAnalysis() // Outputs: // -bool: Person needs update // -string: Person process identifier // -error getPersonProcessIdentifier := func()(bool, string, error){ runningProcessFound, runningProcessIdentifier, err := GetPersonGeneticAnalysisProcessIdentifier(personIdentifier) if (err != nil) { return false, "", err } if (runningProcessFound == true){ // This person has a running analysis. We will show the progress in our current process return true, runningProcessIdentifier, nil } // We check if the person needs a new analysis. getPersonNewAnalysisNeededBool := func()(bool, error){ anyPersonAnalysisFound, _, _, newestPersonAnalysisListOfGenomesAnalyzed, newerAnalysisVersionAvailable, err := GetPersonNewestGeneticAnalysisInfo(personIdentifier) if (err != nil) { return false, err } if (anyPersonAnalysisFound == false){ // No analysis exists. We need to create a new analysis return true, nil } if (newerAnalysisVersionAvailable == true){ // New analysis version is available. return true, nil } allPersonRawGenomeIdentifiersList, err := myGenomes.GetAllPersonRawGenomeIdentifiersList(personAIdentifier) if (err != nil) { return false, err } genomesAreIdentical := helpers.CheckIfTwoListsContainIdenticalItems(allPersonRawGenomeIdentifiersList, newestPersonAnalysisListOfGenomesAnalyzed) if (genomesAreIdentical == false){ // New genome exists/a genome was deleted. A new analysis is needed return true, nil } // Analysis is up to date. Nothing to do. return false, nil } personNewAnalysisIsNeeded, err := getPersonNewAnalysisNeededBool() if (err != nil) { return false, "", err } if (personNewAnalysisIsNeeded == false){ return false, "", nil } // We start a new analysis newProcessIdentifier, err := StartCreateNewPersonGeneticAnalysis(personIdentifier) if (err != nil) { return false, "", err } return true, newProcessIdentifier, nil } personNeedsUpdate, personProcessIdentifier, err := getPersonProcessIdentifier() if (err != nil) { return err } if (personNeedsUpdate == false){ // No analysis needed err := updatePercentageCompleteFunction(personPercentageRangeEnd) if (err != nil) { return err } continue } for{ processFound, processIsComplete, processEncounteredError, errorEncounteredByProcess, processPercentageComplete, err := GetAnalysisProcessInfo(personProcessIdentifier) if (err != nil){ return err } if (processFound == false){ return errors.New("Person process not found after being found already.") } if (processIsComplete == true){ if (processEncounteredError == true){ return errorEncounteredByProcess } err := updatePercentageCompleteFunction(personPercentageRangeEnd) if (err != nil) { return err } break } personPercentageComplete, err := helpers.ScaleNumberProportionally(true, processPercentageComplete, 0, 100, personPercentageRangeStart, personPercentageRangeEnd) if (err != nil) { return err } err = updatePercentageCompleteFunction(personPercentageComplete) if (err != nil) { return err } time.Sleep(100 * time.Millisecond) } } // Both people's analyses are complete. // The percentage progress is at 66% // Now we perform the couple analysis //Outputs: // -[]prepareRawGenomes.RawGenomeWithMetadata: Genome Identifier -> Genome raw data string // -[]string: Person raw genome identifiers list // -error getPersonGenomesList := func(personIdentifier string)([]prepareRawGenomes.RawGenomeWithMetadata, []string, error){ personGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personIdentifier) if (err != nil){ return nil, nil, err } if (len(personGenomesMapList) == 0){ return nil, nil, errors.New("Cannot create person genetic analysis: No genomes found for Person.") } genomeIdentifiersList := make([]string, 0, len(personGenomesMapList)) // Map structure: Genome identifier -> Genome raw data string genomesList := make([]prepareRawGenomes.RawGenomeWithMetadata, 0, len(personGenomesMapList)) for _, genomeMap := range personGenomesMapList{ currentPersonIdentifier, exists := genomeMap["PersonIdentifier"] if (exists == false) { return nil, nil, errors.New("PersonGenomesMapList malformed: Item missing PersonIdentifier") } if (personIdentifier != currentPersonIdentifier){ return nil, nil, errors.New("GetAllPersonGenomesMapList returning genome for different person.") } genomeIdentifier, exists := genomeMap["GenomeIdentifier"] if (exists == false) { return nil, nil, errors.New("PersonGenomesMapList malformed: Item missing GenomeIdentifier") } genomeRawDataString, err := myGenomes.GetGenomeRawDataString(genomeIdentifier) if (err != nil) { return nil, nil, err } rawGenomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, genomeRawDataString) if (err != nil) { return nil, nil, err } if (rawGenomeIsValid == false){ return nil, nil, errors.New("myGenomes contains invalid rawGenomeDataString: " + genomeIdentifier) } genomesList = append(genomesList, rawGenomeWithMetadata) genomeIdentifiersList = append(genomeIdentifiersList, genomeIdentifier) } return genomesList, genomeIdentifiersList, nil } personAGenomesList, personARawGenomeIdentifiersList, err := getPersonGenomesList(personAIdentifier) if (err != nil) { return err } updatePercentageCompleteFunction(70) personBGenomesList, personBRawGenomeIdentifiersList, err := getPersonGenomesList(personBIdentifier) if (err != nil) { return err } updatePercentageCompleteFunction(74) updateCoupleAnalysisPercentageCompleteFunction := func(newPercentage int)error{ personPercentageComplete, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 74, 100) if (err != nil) { return err } err = updatePercentageCompleteFunction(personPercentageComplete) if (err != nil) { return err } return nil } processCompleted, newGeneticAnalysisString, err := createGeneticAnalysis.CreateCoupleGeneticAnalysis(personAGenomesList, personBGenomesList, updateCoupleAnalysisPercentageCompleteFunction, checkIfProcessIsStopped) if (err != nil) { return err } if (processCompleted == false){ // User stopped the analysis mid-way return nil } personAAnalyzedGenomesListString := strings.Join(personARawGenomeIdentifiersList, "+") personBAnalyzedGenomesListString := strings.Join(personBRawGenomeIdentifiersList, "+") analysisIdentifier, err := helpers.GetNewRandomHexString(17) if (err != nil) { return err } currentTime := time.Now().Unix() currentTimeString := helpers.ConvertInt64ToString(currentTime) currentAnalysisVersion := appValues.GetGeneticAnalysisVersion() currentAnalysisVersionString := helpers.ConvertIntToString(currentAnalysisVersion) newAnalysisMap := map[string]string{ "PersonAIdentifier": personAIdentifier, "PersonBIdentifier": personBIdentifier, "AnalysisIdentifier": analysisIdentifier, "TimeCreated": currentTimeString, "PersonAAnalyzedGenomes": personAAnalyzedGenomesListString, "PersonBAnalyzedGenomes": personBAnalyzedGenomesListString, "AnalysisVersion": currentAnalysisVersionString, } updatingMyAnalysesMutex.Lock() defer updatingMyAnalysesMutex.Unlock() err = myCoupleAnalysesMapListDatastore.AddMapListItem(newAnalysisMap) if (err != nil) { return err } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } myAnalysesFolderPath := filepath.Join(userDirectory, "MyAnalyses") analysisFileName := analysisIdentifier + ".json" err = localFilesystem.CreateOrOverwriteFile([]byte(newGeneticAnalysisString), myAnalysesFolderPath, analysisFileName) if (err != nil) { return err } err = updatePercentageCompleteFunction(100) if (err != nil) { return err } coupleAnalysisProcessesMapMutex.Lock() delete(coupleAnalysisProcessesMap, coupleIdentifier) coupleAnalysisProcessesMapMutex.Unlock() return nil } runAnalysis := func(){ err := createNewCoupleGeneticAnalysis() if (err != nil){ appMemory.SetMemoryEntry(newCoupleProcessIdentifier + "_ProgressPercentageComplete", "100") appMemory.SetMemoryEntry(newCoupleProcessIdentifier + "_ErrorEncountered", err.Error()) return } } go runAnalysis() return newCoupleProcessIdentifier, nil }