From 2ab48497797441164e7f57fca2660097d93398ca Mon Sep 17 00:00:00 2001 From: Terry Truong Date: Mon, 25 Apr 2022 01:33:08 +1000 Subject: Adapt to handle open-tree-of-life data Added data_otol/ with script that converts data from 'Open Tree of Life' release 13.4 into a JSON form. Moved old tree-of-life data and images into data_tol_old/. Added TolMap type to tol.ts, changed TolNode, and adapted other code to handle it. Temporarily disabling tile images until image data is added. --- .gitignore | 4 +- data_otol/namedTreeToJSON.py | 181 ++++++++++++++++++ data_tol_old/genTestImgs.sh | 16 ++ data_tol_old/tolData.txt | 388 +++++++++++++++++++++++++++++++++++++++ data_tol_old/txtTreeToJSON.py | 76 ++++++++ src/App.vue | 84 +++++---- src/components/AncestryBar.vue | 10 +- src/components/SearchModal.vue | 13 +- src/components/Tile.vue | 25 ++- src/components/TileInfoModal.vue | 9 +- src/genTestImgs.sh | 16 -- src/layout.ts | 48 ++--- src/tol.ts | 49 +---- src/tolData.txt | 388 --------------------------------------- src/txtTreeToJSON.py | 76 -------- 15 files changed, 778 insertions(+), 605 deletions(-) create mode 100755 data_otol/namedTreeToJSON.py create mode 100755 data_tol_old/genTestImgs.sh create mode 100644 data_tol_old/tolData.txt create mode 100755 data_tol_old/txtTreeToJSON.py delete mode 100755 src/genTestImgs.sh delete mode 100644 src/tolData.txt delete mode 100755 src/txtTreeToJSON.py diff --git a/.gitignore b/.gitignore index 53b49ae..7f8c994 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ node_modules/ dist/ src/tolData.json public/img/ -public/tolgrid-img/ +data_tol_old/img/ +data_tol_old/tolData.json +data_otol/ diff --git a/data_otol/namedTreeToJSON.py b/data_otol/namedTreeToJSON.py new file mode 100755 index 0000000..30b8033 --- /dev/null +++ b/data_otol/namedTreeToJSON.py @@ -0,0 +1,181 @@ +#!/usr/bin/python3 + +import sys, re, json + +usageInfo = f"usage: {sys.argv[0]}\n" +usageInfo += "Reads labelled_supertree_ottnames.tre & annotations.json (from an Open Tree of Life release), \n" +usageInfo += "and prints a JSON object, which maps node names to objects of the form \n" +usageInfo += "{\"children\": [name1, ...], \"parent\": name1, \"tips\": int1, \"pSupport\": bool1}, which holds \n" +usageInfo += "child names, a parent name or null, descendant 'tips', and a phylogeny-support indicator\n" +usageInfo += "\n" +usageInfo += "This script was adapted to handle Open Tree of Life version 13.4.\n" +usageInfo += "Link: https://tree.opentreeoflife.org/about/synthesis-release/v13.4\n" +usageInfo += "\n" +usageInfo += "labelled_supertree_ottnames.tre format:\n" +usageInfo += " Represents a tree-of-life in Newick format, roughly like (n1,n2,(n3,n4)n5)n6,\n" +usageInfo += " where root node is named n6, and has children n1, n2, and n5.\n" +usageInfo += " Name forms include Homo_sapiens_ott770315, mrcaott6ott22687, and 'Oxalis san-miguelii ott5748753'\n" +usageInfo += " Some names can be split up into a 'simple' name (like Homo_sapiens) and an id (like ott770315)\n" +usageInfo += "annotations.json format:\n" +usageInfo += " JSON object holding information about the tree-of-life release.\n" +usageInfo += " The object's 'nodes' field maps node IDs to objects holding information about that node,\n" +usageInfo += " such as phylogenetic trees that support/conflict with it's placement.\n" + +if len(sys.argv) > 1: + print(usageInfo, file=sys.stderr) + sys.exit(1) + +nodeMap = {} # The JSON object to output +idToName = {} # Maps node IDs to names + +# Parse labelled_supertree_ottnames.tre +data = None +with open("labelled_supertree_ottnames.tre") as file: + data = file.read() +dataIdx = 0 +def parseNewick(): + """Parses a node using 'data' and 'dataIdx', updates nodeMap accordingly, and returns the node name or None""" + global dataIdx + # Check for EOF + if dataIdx == len(data): + print("ERROR: Unexpected EOF at index " + str(dataIdx), file=sys.stderr) + return None + # Check for inner-node start + if data[dataIdx] == "(": + dataIdx += 1 + childNames = [] + while True: + # Read child + childName = parseNewick() + if childName == None: + return None + childNames.append(childName) + if (dataIdx == len(data)): + print("ERROR: Unexpected EOF", file=sys.stderr) + return None + # Check for next child + if (data[dataIdx] == ","): + dataIdx += 1 + continue + else: + # Get node name + dataIdx += 1 # Consume an expected ')' + [name, id] = parseNewickName() + idToName[id] = name + # Get child num-tips total + tips = 0 + for childName in childNames: + tips += nodeMap[childName]["tips"] + # Add node to nodeMap + nodeMap[name] = { + "n": name, "id": id, "children": childNames, "parent": None, "tips": tips, "pSupport": False + } + # Update childrens' parent reference + for childName in childNames: + nodeMap[childName]["parent"] = name + return name + else: + [name, id] = parseNewickName() + idToName[id] = name + nodeMap[name] = {"n": name, "id": id, "children": [], "parent": None, "tips": 1, "pSupport": False} + return name +def parseNewickName(): + """Helper that parses an input node name, and returns a [name,id] pair""" + global data, dataIdx + name = None + end = dataIdx + # Get name + if (end < len(data) and data[end] == "'"): # Check for quoted name + end += 1 + inQuote = True + while end < len(data): + if (data[end] == "'"): + if end + 1 < len(data) and data[end+1] == "'": # Account for '' as escaped-quote + end += 2 + continue + else: + end += 1 + inQuote = False + break + end += 1 + if inQuote: + raise Exception("ERROR: Unexpected EOF") + name = data[dataIdx:end] + dataIdx = end + else: + while end < len(data) and not re.match(r"[(),]", data[end]): + end += 1 + if (end == dataIdx): + raise Exception("ERROR: Unexpected EOF") + name = data[dataIdx:end].rstrip() + if end == len(data): # Ignore trailing input semicolon + name = name[:-1] + dataIdx = end + # Convert to [name, id] + if name.startswith("mrca"): + return [name, name] + elif name[0] == "'": + match = re.fullmatch(r"'([^\\\"]+) (ott\d+)'", name) + if match == None: + raise Exception("ERROR: invalid name \"{}\"".format(name)) + name = match.group(1).replace("''", "'") + return [name, match.group(2)] + else: + match = re.fullmatch(r"([^\\\"]+)_(ott\d+)", name) + if match == None: + raise Exception("ERROR: invalid name \"{}\"".format(name)) + return [match.group(1).replace("_", " "), match.group(2)] +rootName = parseNewick() + +# Parse annotations.json +data = None +with open("annotations.json") as file: + data = file.read() +obj = json.loads(data) +nodeAnnsMap = obj['nodes'] + +# Do some more postprocessing on each node +def convertMrcaName(name): + """Given an mrca* name, returns an expanded version with the form [name1 + name2]""" + match = re.fullmatch(r"mrca(ott\d+)(ott\d+)", name) + if match == None: + print("ERROR: Invalid name \"{}\"".format(name), file=sys.stderr) + else: + subName1 = match.group(1) + subName2 = match.group(2) + if subName1 not in idToName: + print("ERROR: MRCA name \"{}\" sub-name \"{}\" not found".format(subName1), file=sys.stderr) + elif subName2 not in idToName: + print("ERROR: MRCA name \"{}\" sub-name \"{}\" not found".format(subName2), file=sys.stderr) + else: + return "[{} + {}]".format(idToName[subName1], idToName[subName2]) +namesToSwap = [] # Will hold [oldName, newName] pairs, for renaming nodes in nodeMap +for node in nodeMap.values(): + # Set has-support value using annotations + id = node["id"] + if id in nodeAnnsMap: + nodeAnns = nodeAnnsMap[id] + supportQty = len(nodeAnns["supported_by"]) if "supported_by" in nodeAnns else 0 + conflictQty = len(nodeAnns["conflicts_with"]) if "conflicts_with" in nodeAnns else 0 + node["pSupport"] = supportQty > 0 and conflictQty == 0 + # Change mrca* names + name = node["n"] + if (name.startswith("mrca")): + namesToSwap.append([name, convertMrcaName(name)]) + parentName = node["parent"] + if (parentName != None and parentName.startswith("mrca")): + node["parent"] = convertMrcaName(parentName) + childNames = node["children"] + for i in range(len(childNames)): + if (childNames[i].startswith("mrca")): + childNames[i] = convertMrcaName(childNames[i]) + # Delete some no-longer-needed fields + del node["n"] + del node["id"] +# Finish mrca* renamings +for [oldName, newName] in namesToSwap: + nodeMap[newName] = nodeMap[oldName] + del nodeMap[oldName] + +# Output JSON +print(json.dumps(nodeMap)) diff --git a/data_tol_old/genTestImgs.sh b/data_tol_old/genTestImgs.sh new file mode 100755 index 0000000..21b001b --- /dev/null +++ b/data_tol_old/genTestImgs.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +#generate tol.json from tol.txt +cat tolData.txt | ./txtTreeToJSON.py > tolData.json + +#reads through tolData.json, gets names, and generates image for each name +cat tolData.json | \ + gawk 'match ($0, /"name"\s*:\s*"([^"]*)"/, arr) {print arr[1]}' | \ + while read; do + convert -size 200x200 xc:khaki +repage \ + -size 150x150 -fill black -background None \ + -font Ubuntu-Mono -gravity center caption:"$REPLY" +repage \ + -gravity Center -composite -strip ../public/img/"$REPLY".png + done + diff --git a/data_tol_old/tolData.txt b/data_tol_old/tolData.txt new file mode 100644 index 0000000..f73a064 --- /dev/null +++ b/data_tol_old/tolData.txt @@ -0,0 +1,388 @@ +Tree of Life + Viruses + Caudovirales + Herpesvirales + Ligamenvirales + Mononegavirales + Nidovirales + Picornavirales + Tymovirales + Archaea + Crenarchaeota + Euryarchaeota + Bacteria + Acidobacteria + Actinobacteria + Aquificae + Armatimonadetes + Bacteroidetes + Caldiserica + Chlamydiae + Chlorobi + Chloroflexi + Chrysiogenetes + Cyanobacteria + Deferribacteres + Deinococcus-thermus + Dictyoglomi + Elusimicrobia + Fibrobacteres + Firmicutes + Fusobacteria + Gemmatimonadetes + Lentisphaerae + Nitrospira + Planctomycetes + Proteobacteria + Spirochaetae + Synergistetes + Tenericutes + Thermodesulfobacteria + Thermotogae + Verrucomicrobia + Eukaryota + Diatoms + Amoebozoa + Plantae + Rhodopyhta + Viridiplantae + Prasinophytes + Ulvophyceae + Streptophyta + Charales + Embryophytes + Marchantiomorpha + Anthocerotophyta + Bryophyta + Lycopodiopsida + Lycopodiidae + Selaginellales + Polypodiopsida + Polypodiidae + Polypodiales + Equisetidae + Spermatopsida + Cycads + Conifers + Taxaceae + Cupressaceae + Pinaceae + Pinus + Picea + Larix + Cedrus + Abies + Ginkgos + Angiosperms + Illicium + magnoliids + Piperales + Piperaceae + Magnoliales + Annonaceae + Myristicaceae + Laurales + Lauraceae + Monocotyledons + Alismatanae + Aranae + Liliaceae + Asparagales + Amaryllidaceae + Asparagaceae + Asphodelaceae + Iridaceae + Orchidaceae + Dioscoreaceae + Arecanae + Cocoeae + Phoeniceae + Zingiberanae + Musaceae + Strelitziaceae + Zingiberaceae + Commelinanae + Bromeliaceae + Cyperaceae + Typhaceae + Poaceae + Zea mays + Triticum + Bambusoideae + eudicots + Ranunculales + Papaveraceae + Ranunculaceae + Proteales + Proteaceae + Nelumbo + Core Eudicots + Saxifragales + Rosids + Fabaceae + Mimosoideae + IRLC (Inverted Repat-lacking clade) + Trifolieae + Fabeae + Rosales + Rosaceae + Rosa + Malus pumila + Ulmaceae + Urticaceae + Moraceae + Cannabaceae + Fagales + Fagaceae + Betulaceae + Juglandaceae + Cucurbitales + Cucurbitaceae + Malpighiales + Salicaceae + Violaceae + Passifloraceae + Erythroxylaceae + Rhizophoraceae + Euphorbiaceae + Linaceae + Rafflesiaceae + Myrtales + Myrtaceae + Onagraceae + Lythraceae + Brassicales + Caricaceae + Brassicaceae + Malvales + Core Malvales + Malvoideae + Bombacoideae + Sterculioideae + Helicteroideae + Byttnerioideae + Sapindales + Anacardiaceae + Burseraceae + Meliaceae + Rutaceae + Sapindaceae + Vitaceae + Caryophyllales + Polygonaceae + Droseraceae + Nepenthaceae + core Caryophyllales + Cactaceae + Amaranthaceae + Asterids + Ericales + Actinidiaceae + Ericaceae + Lecythidaceae + Sapotaceae + Ebenaceae + Theaceae + Solanales + Solanaceae + Convolvulaceae + Lamiales + Oleaceae + Fraxinus + Bignoniaceae + Pedaliaceae + Lentibulariaceae + Lamiaceae + Gentianales + Rubiaceae + Asterales + Campanulaceae + Asteraceae + Carduoideae + Cardueae + Cichorioideae + Cichorieae + Asteroideae + Asterodae + Helianthodae + Apiales + Apiaceae + Araliaceae + Aquifoliaceae + Fungi + Fungi 1 + Dikarya + Basidiomycota + Agaricomycotina + Agaricomycetes + Agaricomycetes 1 + Agaricomycetidae + Agaricales + Strophariaceae strict-sense + Psathyrellaceae + Agaricaceae + Nidulariaceae + Marasmiaceae + Physalacriaceae + Pleurotaceae + Amanitaceae + Podoserpula + Boletales + Serpulaceae + Sclerodermataceae + Boletaceae + Russulales + Hymenochaetales + Phallomycetidae + Geastrales + Gomphales + Phallales + Cantharellales + Auriculariales + Tremellomycetes + Ustilaginomycotina + Pucciniomycotina + Pucciniomycetes + Septobasidiales + Pucciniales + Mixiomycetes + Tritirachiomycetes + Entorrhizomycetes + Wallemiomycetes + Ascomycota + Pezizomycotina + Pezizomycetes + 'Leotiomyceta' + Eurotiomycetes + Geoglossaceae + Sordariomycetes + Hypocreomycetidae + Sordariomycetidae + Laboulbeniomycetes + Pleosporomycetidae + Saccharomycotina + Taphrinomycotina + Schizosaccharomycetes + Pneumocystidiomycetes + Taphrinomycetes + Glomeromycota + Zygomycota + Endogonales + Mucorales + Blastocladiomycota + Chytridiomycota + Neocallimastigomycota + Microsporidia + Animalia + Porifera + Cnidaria + Tardigrada + Annelida + Mollusca + Bivalvia + Gastropoda + Cephalopoda + Arthropoda + Arachnida + Araneae + Opiliones + Scorpiones + Heterostigmata + Crustacea + Euphausiacea + Brachyura + Isopoda + Cirripedia + Insecta + Anisoptera + Mantodea + Cicadoidea + Siphonaptera + Cucujoidea + Phengodidae + Drosophilidae + Culicidae + Lepidoptera + Apini + Formicidae + Deuterostomia + Echinodermata + Crinoidea + Asteroidea + Echinoidea + Holothuroidea + Vertebrata + Chondrichthyes + Carcharodon carcharias + Rhinocodon typus + Batoidea + Pristidae + Actinopterygii + Clupeomorpha + Xiphias gladius + Siluriformes + Carassius auratus + Tetraodontidae + Molidae + Gymnotiformes + Lophiiformes + Exocoetidae + 'mudskipper' + Hippocampus + Psudoliparis swirei + Sarcopterygii + Tetrapoda + Amphibia + Gymnophiona + Caudata + Salamandra + Cryptobranchidae + Ambystomatidae + Anura + Reptilia + Testudines + Plesiosauria + Chamaeleonidae + Serpentes + Crocodilia + Dinosauria + Triceratops + Sauropoda + Tyrannosauroidea + Aves + magpie + parrot + eagle + owl + swan + chicken + penguin + hummingbird + Synapsida + monotreme + marsupial + kangaroo + possum + wombat + rodent + mouse + beaver + rabbit + feline + canine + bear + walrus + Artiodactyla + pig + camel + deer + giraffe + horse + elephant + cetacean + armadillo + bat + monkey + gorilla + chimpanzee + homo sapien diff --git a/data_tol_old/txtTreeToJSON.py b/data_tol_old/txtTreeToJSON.py new file mode 100755 index 0000000..3b77622 --- /dev/null +++ b/data_tol_old/txtTreeToJSON.py @@ -0,0 +1,76 @@ +#!/usr/bin/python3 + +import sys, re + +usageInfo = f"usage: {sys.argv[0]}\n" +usageInfo += "Reads, from stdin, tab-indented lines representing trees, and outputs corresponding JSON.\n" + +if len(sys.argv) > 1: + print(usageInfo, file=sys.stderr) + sys.exit(1) + +lineNum = 0 +trees = [] #each node is a pair holding a name and an array of child nodes +nodeList = [] +while True: + #read line + line = sys.stdin.readline() + if line == "": break + line = line.rstrip() + lineNum += 1 + #create node + match = re.match(r"^\t*", line) + indent = len(match.group()) + newNode = [line[indent:], []] + #add node + if indent == len(nodeList): #sibling or new tree + if len(nodeList) == 0: + nodeList.append(newNode) + trees.append(newNode) + else: + nodeList[-1] = newNode + if len(nodeList) == 1: + trees[-1][1].append(newNode) + else: + nodeList[-2][1].append(newNode) + elif indent == len(nodeList) + 1: #direct child + if len(nodeList) == 0: + print(f"ERROR: Child without preceding root (line {lineNum})") + sys.exit(1) + nodeList.append(newNode) + nodeList[-2][1].append(newNode) + elif indent < len(nodeList): #ancestor sibling or new tree + nodeList = nodeList[:indent] + if len(nodeList) == 0: + nodeList.append(newNode) + trees.append(newNode) + else: + nodeList[-1] = newNode + if len(nodeList) == 1: + trees[-1][1].append(newNode) + else: + nodeList[-2][1].append(newNode) + else: + print(f"ERROR: Child with invalid relative indent (line {lineNum})") + sys.exit(1) +#print as JSON +if len(trees) > 1: + print("[") +def printNode(node, indent): + if len(node[1]) == 0: + print(indent + "{\"name\": \"" + node[0] + "\"}", end="") + else: + print(indent + "{\"name\": \"" + node[0] + "\", \"children\": [") + for i in range(len(node[1])): + printNode(node[1][i], indent + "\t") + if i < len(node[1])-1: + print(",", end="") + print() + print(indent + "]}", end="") +for i in range(len(trees)): + printNode(trees[i], "") + if i < len(trees)-1: + print(",", end="") + print() +if len(trees) > 1: + print("]") diff --git a/src/App.vue b/src/App.vue index cf25b18..e00072b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,7 +13,8 @@ import SearchIcon from './components/icon/SearchIcon.vue'; import PlayIcon from './components/icon/PlayIcon.vue'; import SettingsIcon from './components/icon/SettingsIcon.vue'; // Other -import {TolNode, TolNodeRaw, tolFromRaw, getTolMap} from './tol'; +import type {TolMap} from './tol'; +import {TolNode} from './tol'; import {LayoutNode, initLayoutTree, initLayoutMap, tryLayout} from './layout'; import type {LayoutOptions} from './layout'; import {arraySum, randWeightedChoice} from './util'; @@ -38,9 +39,9 @@ function getReverseAction(action: Action): Action | null { } // Get tree-of-life data -import tolRaw from './tolData.json'; -const tol = tolFromRaw(tolRaw); -const tolMap = getTolMap(tol); +import data from './tolData.json'; +let tolMap: TolMap = data; +const rootName = "[Elaeocarpus williamsianus + Brunellia mexicana]"; // Configurable options const defaultLytOpts: LayoutOptions = { @@ -89,14 +90,14 @@ const defaultUiOpts = { export default defineComponent({ data(){ - let layoutTree = initLayoutTree(tol, 0); + let layoutTree = initLayoutTree(tolMap, rootName, 0); return { + tolMap: tolMap, layoutTree: layoutTree, activeRoot: layoutTree, // Differs from layoutTree root when expand-to-view is used layoutMap: initLayoutMap(layoutTree), // Maps names to LayoutNode objects - tolMap: tolMap, // Maps names to TolNode objects // Modals and settings related - infoModalNode: null as TolNode | null, // Node to display info for, or null + infoModalNode: null as LayoutNode | null, // Node to display info for, or null helpOpen: false, searchOpen: false, settingsOpen: false, @@ -170,16 +171,22 @@ export default defineComponent({ methods: { // For tile expand/collapse events onLeafClick(layoutNode: LayoutNode){ - let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, - {allowCollapse: false, chg: {type: 'expand', node: layoutNode}, layoutMap: this.layoutMap}); + let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, { + allowCollapse: false, + chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap}, + layoutMap: this.layoutMap + }); if (!success){ layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation } return success; }, onNonleafClick(layoutNode: LayoutNode){ - let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, - {allowCollapse: false, chg: {type: 'collapse', node: layoutNode}, layoutMap: this.layoutMap}); + let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, { + allowCollapse: false, + chg: {type: 'collapse', node: layoutNode, tolMap: this.tolMap}, + layoutMap: this.layoutMap + }); if (!success){ layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation } @@ -193,8 +200,11 @@ export default defineComponent({ } LayoutNode.hideUpward(layoutNode); this.activeRoot = layoutNode; - tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, - {allowCollapse: true, chg: {type: 'expand', node: layoutNode}, layoutMap: this.layoutMap}); + tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, { + allowCollapse: true, + chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap}, + layoutMap: this.layoutMap + }); }, onNonleafClickHeld(layoutNode: LayoutNode){ if (layoutNode == this.activeRoot){ @@ -204,7 +214,7 @@ export default defineComponent({ LayoutNode.hideUpward(layoutNode); this.activeRoot = layoutNode; tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, - {allowCollapse: true, layoutMap: this.layoutMap, }); + {allowCollapse: true, layoutMap: this.layoutMap}); }, onDetachedAncestorClick(layoutNode: LayoutNode){ LayoutNode.showDownward(layoutNode); @@ -215,7 +225,7 @@ export default defineComponent({ // For tile-info events onInfoIconClick(node: LayoutNode){ this.resetMode(); - this.infoModalNode = node.tolNode; + this.infoModalNode = node; }, // For help events onHelpIconClick(){ @@ -227,60 +237,58 @@ export default defineComponent({ this.resetMode(); this.searchOpen = true; }, - onSearchNode(tolNode: TolNode){ + onSearchNode(name: string){ this.searchOpen = false; this.modeRunning = true; - this.expandToTolNode(tolNode); + this.expandToNode(name); }, - expandToTolNode(tolNode: TolNode){ + expandToNode(name: string){ if (!this.modeRunning){ return; } // Check if searched node is displayed - let layoutNode = this.layoutMap.get(tolNode.name); - if (layoutNode != null && !layoutNode.hidden){ - this.setLastFocused(layoutNode); + let layoutNodeVal = this.layoutMap.get(name); + if (layoutNodeVal != null && !layoutNodeVal.hidden){ + this.setLastFocused(layoutNodeVal); this.modeRunning = false; return; } // Get nearest in-layout-tree ancestor - let ancestor = tolNode; - while (this.layoutMap.get(ancestor.name) == null){ - ancestor = ancestor.parent!; + let ancestorName = name; + while (this.layoutMap.get(ancestorName) == null){ + ancestorName = this.tolMap[ancestorName].parent!; } - layoutNode = this.layoutMap.get(ancestor.name)!; + let layoutNode = this.layoutMap.get(ancestorName)!; // If hidden, expand self/ancestor in ancestry-bar if (layoutNode.hidden){ while (!this.detachedAncestors!.includes(layoutNode)){ - ancestor = ancestor.parent!; - layoutNode = this.layoutMap.get(ancestor.name)!; + layoutNode = layoutNode.parent!; } this.onDetachedAncestorClick(layoutNode!); - setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration); + setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration); return; } // Attempt tile-expand let success = this.onLeafClick(layoutNode); if (success){ - setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration); + setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration); return; } // Attempt expand-to-view on ancestor just below activeRoot - if (ancestor.name == this.activeRoot.tolNode.name){ + if (layoutNode == this.activeRoot){ console.log('Unable to complete search (not enough room to expand active root)'); // Note: Only happens if screen is significantly small or node has significantly many children this.modeRunning = false; return; } while (true){ - if (ancestor.parent!.name == this.activeRoot.tolNode.name){ + if (layoutNode.parent! == this.activeRoot){ break; } - ancestor = ancestor.parent!; + layoutNode = layoutNode.parent!; } - layoutNode = this.layoutMap.get(ancestor.name)!; this.onNonleafClickHeld(layoutNode); - setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration); + setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration); }, // For auto-mode events onPlayIconClick(){ @@ -317,7 +325,7 @@ export default defineComponent({ if (node == this.activeRoot){ actionWeights['move up'] = 0; } - if (node.tolNode.children.length == 0){ + if (this.tolMap[node.name].children.length == 0){ actionWeights['expand'] = 0; } } else { @@ -463,13 +471,13 @@ export default defineComponent({