diff options
| -rwxr-xr-x | backend/server.py | 87 | ||||
| -rw-r--r-- | src/App.vue | 10 | ||||
| -rw-r--r-- | src/README.md | 2 | ||||
| -rw-r--r-- | src/components/TileInfoModal.vue | 240 | ||||
| -rw-r--r-- | src/layout.ts | 2 | ||||
| -rw-r--r-- | src/lib.ts | 15 |
6 files changed, 172 insertions, 184 deletions
diff --git a/backend/server.py b/backend/server.py index f73ef29..31d9e25 100755 --- a/backend/server.py +++ b/backend/server.py @@ -68,12 +68,17 @@ class ImgInfo: self.license = license # string self.artist = artist # string self.credit = credit # string +class NodeInfo: + " Represents info about a node " + def __init__(self, tolNode, descInfo, imgInfo): + self.tolNode = tolNode # TolNode + self.descInfo = descInfo # null | DescInfo + self.imgInfo = imgInfo # null | ImgInfo class InfoResponse: " Sent as responses to 'info' requests " - def __init__(self, tolNode, descData, imgData): - self.tolNode = tolNode # null | TolNode - self.descData = descData # null | DescInfo | [DescInfo, DescInfo] - self.imgData = imgData # null | ImgInfo | [ImgInfo, ImgInfo] + def __init__(self, nodeInfo, subNodesInfo = None): + self.nodeInfo = nodeInfo # NodeInfo + self.subNodesInfo = subNodesInfo # [] | [NodeInfo, NodeInfo] # Connect to db dbCon = sqlite3.connect(dbFile) @@ -194,48 +199,42 @@ def lookupNodeInfo(name, useReducedTree): tolNode = nameToNodes[name] if name in nameToNodes else None if tolNode == None: return None - # Get node desc - descData = None + # Check for compound node match = re.fullmatch(r"\[(.+) \+ (.+)]", name) - if match == None: - query = "SELECT desc, wiki_id, redirected, from_dbp FROM" \ - " wiki_ids INNER JOIN descs ON wiki_ids.id = descs.wiki_id WHERE wiki_ids.name = ?" - row = cur.execute(query, (name,)).fetchone() - if row != None: - (desc, wikiId, redirected, fromDbp) = row - descData = DescInfo(desc, wikiId, redirected == 1, fromDbp == 1) - else: - # Get descs for compound-node element - descData = [None, None] - query = "SELECT name, desc, wiki_id, redirected, from_dbp FROM" \ - " wiki_ids INNER JOIN descs ON wiki_ids.id = descs.wiki_id WHERE wiki_ids.name IN (?, ?)" - for (nodeName, desc, wikiId, redirected, fromDbp) in cur.execute(query, match.group(1,2)): - idx = 0 if nodeName == match.group(1) else 1 - descData[idx] = DescInfo(desc, wikiId, redirected == 1, fromDbp == 1) + subNames = [match.group(1), match.group(2)] if match != None else [] + if len(subNames) > 0: + nameToSubNodes = lookupNodes(subNames, useReducedTree) + if len(nameToSubNodes) < 2: + print(f"ERROR: Unable to find sub-names entries for {name}", file=sys.stderr) + return None + nameToNodes.update(nameToSubNodes) + namesToLookup = [name] if len(subNames) == 0 else subNames + # Get desc info + nameToDescInfo = {} + query = "SELECT name, desc, wiki_id, redirected, from_dbp FROM" \ + " wiki_ids INNER JOIN descs ON wiki_ids.id = descs.wiki_id" \ + " WHERE wiki_ids.name IN ({})".format(",".join(["?"] * len(namesToLookup))) + for (nodeName, desc, wikiId, redirected, fromDbp) in cur.execute(query, namesToLookup): + nameToDescInfo[nodeName] = DescInfo(desc, wikiId, redirected == 1, fromDbp == 1) # Get image info - imgData = None - if isinstance(tolNode.imgName, str): - otolId = tolNode.imgName[:-4] # Convert filename excluding .jpg suffix - query = "SELECT images.id, images.src, url, license, artist, credit FROM" \ - " nodes INNER JOIN node_imgs ON nodes.name = node_imgs.name" \ - " INNER JOIN images ON node_imgs.img_id = images.id AND node_imgs.src = images.src" \ - " WHERE nodes.id = ?" - (imgId, imgSrc, url, license, artist, credit) = cur.execute(query, (otolId,)).fetchone() - imgData = ImgInfo(imgId, imgSrc, url, license, artist, credit) - elif isinstance(tolNode.imgName, list): - # Get info for compound-image parts - imgData = [None, None] - idsToLookup = [n[:-4] for n in tolNode.imgName if n != None] - query = "SELECT nodes.id, images.id, images.src, url, license, artist, credit FROM" \ - " nodes INNER JOIN node_imgs ON nodes.name = node_imgs.name" \ - " INNER JOIN images ON node_imgs.img_id = images.id AND node_imgs.src = images.src" \ - " WHERE nodes.id IN ({})".format(",".join(["?"] * len(idsToLookup))) - for (imgOtolId, imgId, imgSrc, url, license, artist, credit) in cur.execute(query, idsToLookup): - imgName1 = tolNode.imgName[0] - idx = 0 if (imgName1 != None and imgOtolId == imgName1[:-4]) else 1 - imgData[idx] = ImgInfo(imgId, imgSrc, url, license, artist, credit) - # - return InfoResponse(tolNode, descData, imgData) + nameToImgInfo = {} + idsToNames = {nameToNodes[n].imgName[:-4]: n for n in namesToLookup if nameToNodes[n].imgName != None} + idsToLookup = list(idsToNames.keys()) # Lookup using IDs avoids having to check linked_imgs + query = "SELECT nodes.id, images.id, images.src, url, license, artist, credit FROM" \ + " nodes INNER JOIN node_imgs ON nodes.name = node_imgs.name" \ + " INNER JOIN images ON node_imgs.img_id = images.id AND node_imgs.src = images.src" \ + " WHERE nodes.id IN ({})".format(",".join(["?"] * len(idsToLookup))) + for (id, imgId, imgSrc, url, license, artist, credit) in cur.execute(query, idsToLookup): + nameToImgInfo[idsToNames[id]] = ImgInfo(imgId, imgSrc, url, license, artist, credit) + # Construct response + nodeInfoObjs = [ + NodeInfo( + nameToNodes[n], + nameToDescInfo[n] if n in nameToDescInfo else None, + nameToImgInfo[n] if n in nameToImgInfo else None + ) for n in [name] + subNames + ] + return InfoResponse(nodeInfoObjs[0], nodeInfoObjs[1:]) class DbServer(BaseHTTPRequestHandler): " Provides handlers for requests to the server " diff --git a/src/App.vue b/src/App.vue index 7576278..a3d0559 100644 --- a/src/App.vue +++ b/src/App.vue @@ -59,7 +59,8 @@ function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions { let screenSz = getBreakpoint(); // Reused option values let textColor = '#fafaf9'; - let bgColor = '#292524', bgColorLight = '#44403c', bgColorDark = '#1c1917', + let bgColor = '#292524', bgColorAlt = '#fafaf9', + bgColorLight = '#44403c', bgColorDark = '#1c1917', bgColorLight2 = '#57534e', bgColorDark2 = '#0e0c0b'; let altColor = '#a3e623', altColorDark = '#65a30d'; let accentColor = '#f59e0b'; @@ -69,6 +70,7 @@ function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions { // Shared coloring/sizing textColor, bgColor, + bgColorAlt, bgColorLight, bgColorDark, bgColorLight2, @@ -256,7 +258,7 @@ export default defineComponent({ let response = await fetch(urlPath); responseObj = await response.json(); } catch (error){ - console.log('ERROR loading tolnode data', error); + console.log('ERROR: Unable to retreive tol-node data', error); return false; } Object.getOwnPropertyNames(responseObj).forEach(n => {this.tolMap.set(n, responseObj[n])}); @@ -354,7 +356,7 @@ export default defineComponent({ let response = await fetch(urlPath); responseObj = await response.json(); } catch (error){ - console.log('ERROR loading tolnode data', error); + console.log('ERROR: Unable to retreive tol-node data', error); return false; } Object.getOwnPropertyNames(responseObj).forEach(n => {this.tolMap.set(n, responseObj[n])}); @@ -813,7 +815,7 @@ export default defineComponent({ let response = await fetch(urlPath); responseObj = await response.json(); } catch (error) { - console.log('ERROR: Unable to get tree data', error); + console.log('ERROR: Unable to retrieve tree data', error); return; } // Get root node name diff --git a/src/README.md b/src/README.md index dbd2ecb..6bf764c 100644 --- a/src/README.md +++ b/src/README.md @@ -2,7 +2,7 @@ - main.ts: Included by ../index.html. Creates the main Vue component. - App.vue: The main Vue component. - components: - - Tile.vue: Displays a tree-of-life node, and can include child nodes. + - Tile.vue: Displays a tree-of-life node. - TileInfoModal.vue: Modal displaying info about a Tile's node. - SearchModal.vue: Modal with a search bar. - SettingsModal: Modal displaying configurable settings. diff --git a/src/components/TileInfoModal.vue b/src/components/TileInfoModal.vue index 90ad9a7..c8f8047 100644 --- a/src/components/TileInfoModal.vue +++ b/src/components/TileInfoModal.vue @@ -1,76 +1,66 @@ <script lang="ts"> + import {defineComponent, PropType} from 'vue'; import CloseIcon from './icon/CloseIcon.vue'; -import Tile from './Tile.vue' import {LayoutNode, LayoutOptions} from '../layout'; -import {TolNode, TolMap, UiOptions, DescInfo, ImgInfo, TileInfoResponse} from '../lib'; +import {TolNode, TolMap, UiOptions, DescInfo, ImgInfo, NodeInfo, InfoResponse} from '../lib'; import {capitalizeWords} from '../util'; -// Displays information about a tree-of-life node export default defineComponent({ - data(){ - return { - tolNode: null as null | TolNode, - descInfo: null as null | DescInfo, - descInfo1: null as null | DescInfo, - descInfo2: null as null | DescInfo, - imgInfo: null as null | ImgInfo, - imgInfo1: null as null | ImgInfo, - imgInfo2: null as null | ImgInfo, - }; - }, props: { nodeName: {type: String, required: true}, tolMap: {type: Object as PropType<TolMap>, required: true}, + // Options lytOpts: {type: Object as PropType<LayoutOptions>, required: true}, uiOpts: {type: Object as PropType<UiOptions>, required: true}, }, + data(){ + return { + // These are set using a server response + tolNode: null as null | TolNode, + nodes: [] as TolNode[], // The nodes to display info for + imgInfos: [] as (ImgInfo | null)[], + descInfos: [] as (DescInfo | null)[], + }; + }, computed: { - displayName(): string { - if (this.tolNode == null || this.tolNode.commonName == null){ - return capitalizeWords(this.nodeName); - } else { - return `${capitalizeWords(this.tolNode.commonName)} (aka ${capitalizeWords(this.nodeName)})`; - } - }, - subName1(): string { - return this.displayName.substring(1, this.displayName.indexOf(' + ')); - }, - subName2(): string { - return this.displayName.substring(this.displayName.indexOf(' + ') + 3, this.displayName.length - 1); - }, - imgStyles(): Record<string,string> { - return this.getImgStyles(this.tolNode == null ? null : this.tolNode.imgName as string); - }, - firstImgStyles(): Record<string,string> { - return this.getImgStyles(this.tolNode!.imgName![0]); - }, - secondImgStyles(): Record<string,string> { - return this.getImgStyles(this.tolNode!.imgName![1]); + subNames(): [string, string] | null { + const regex = /\[(.+) \+ (.+)\]/; + let results = regex.exec(this.nodeName); + return results == null ? null : [results[1], results[2]]; }, - dummyNode(): LayoutNode { - let newNode = new LayoutNode(this.nodeName, []); - newNode.dims = [this.lytOpts.maxTileSz, this.lytOpts.maxTileSz]; - return newNode; + styles(): Record<string,string> { + return { + backgroundColor: this.uiOpts.bgColorAlt, + borderRadius: this.uiOpts.borderRadius + 'px', + boxShadow: this.uiOpts.shadowNormal, + overflow: 'visible auto', + }; }, }, methods: { - onCloseClick(evt: Event){ - if (evt.target == this.$el || (this.$refs.closeIcon as typeof CloseIcon).$el.contains(evt.target)){ - this.$emit('close'); + getDisplayName(name: string, tolNode: TolNode | null): string { + if (tolNode == null || tolNode.commonName == null){ + return capitalizeWords(name); + } else { + return `${capitalizeWords(tolNode.commonName)} (aka ${capitalizeWords(name)})`; } }, - getImgStyles(imgName: string | null){ + getImgStyles(tolNode: TolNode): Record<string,string> { + let imgName = null; + if (typeof(tolNode.imgName) === 'string'){ // Exclude string-array case + imgName = tolNode.imgName; + } return { - boxShadow: this.uiOpts.shadowNormal, - borderRadius: this.uiOpts.borderRadius + 'px', + width: this.lytOpts.maxTileSz + 'px', + height: this.lytOpts.maxTileSz + 'px', backgroundImage: imgName != null ? 'url(\'/img/' + imgName.replaceAll('\'', '\\\'') + '\')' : 'none', - backgroundColor: '#1c1917', + backgroundColor: this.uiOpts.bgColorDark, backgroundSize: 'cover', - width: this.lytOpts.maxTileSz + 'px', - height: this.lytOpts.maxTileSz + 'px', + borderRadius: this.uiOpts.borderRadius + 'px', + boxShadow: this.uiOpts.shadowNormal, }; }, licenseToUrl(license: string){ @@ -103,41 +93,54 @@ export default defineComponent({ return "[INVALID LICENSE]"; } }, + onClose(evt: Event){ + if (evt.target == this.$el || (this.$refs.closeIcon as typeof CloseIcon).$el.contains(evt.target)){ + this.$emit('close'); + } + }, }, - created(){ + async created(){ + // Query server for tol-node info let url = new URL(window.location.href); url.pathname = '/data/info'; url.search = '?name=' + encodeURIComponent(this.nodeName); - url.search += this.uiOpts.useReducedTree ? '&tree=reduced' : ''; - fetch(url.toString()) - .then(response => response.json()) - .then(obj => { - this.tolNode = obj.tolNode; - if (!Array.isArray(obj.descData)){ - this.descInfo = obj.descData; - } else { - [this.descInfo1, this.descInfo2] = obj.descData; - } - if (!Array.isArray(obj.imgData)){ - this.imgInfo = obj.imgData; - } else { - [this.imgInfo1, this.imgInfo2] = obj.imgData; - } - }); + if (this.uiOpts.useReducedTree){ + url.search += '&tree=reduced'; + } + let responseObj: InfoResponse; + try { + let response = await fetch(url.toString()); + responseObj = await response.json(); + } catch (error){ + console.log("ERROR: Unable to retrieve data from server") + return; + } + // Set fields from response + this.tolNode = responseObj.nodeInfo.tolNode; + if (responseObj.subNodesInfo.length == 0){ + this.nodes = [this.tolNode] + this.imgInfos = [responseObj.nodeInfo.imgInfo]; + this.descInfos = [responseObj.nodeInfo.descInfo]; + } else { + for (let nodeInfo of responseObj.subNodesInfo){ + this.nodes.push(nodeInfo.tolNode); + this.imgInfos.push(nodeInfo.imgInfo); + this.descInfos.push(nodeInfo.descInfo); + } + } }, - components: {CloseIcon, Tile, }, + components: {CloseIcon, }, emits: ['close', ], }); </script> <template> -<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onCloseClick"> - <div class="absolute left-1/2 -translate-x-1/2 w-4/5 max-h-[80%] overflow-y-auto top-1/2 -translate-y-1/2 p-4 - bg-stone-50 rounded-md shadow shadow-black"> - <close-icon @click.stop="onCloseClick" ref="closeIcon" +<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose"> + <div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 w-4/5 max-h-[80%] p-4" :style="styles"> + <close-icon @click.stop="onClose" ref="closeIcon" class="block absolute top-2 right-2 w-6 h-6 hover:cursor-pointer"/> <h1 class="text-center text-xl font-bold mb-2"> - {{displayName}} + {{getDisplayName(nodeName, tolNode)}} <div v-if="tolNode != null"> ({{tolNode.children.length}} children, {{tolNode.tips}} tips, <a :href="'https://tree.opentreeoflife.org/opentree/argus/opentree13.4@' + tolNode.otolId"> @@ -145,66 +148,45 @@ export default defineComponent({ </div> </h1> <hr class="mb-4 border-stone-400"/> - <div class="flex"> - <div class="mr-4"> - <div v-if="tolNode == null"/> - <div v-else-if="!Array.isArray(tolNode.imgName)"> - <div :style="imgStyles"/> - <ul v-if="imgInfo != null"> - <li>Obtained via: {{imgInfo.src}}</li> - <li>License: <a :href="licenseToUrl(imgInfo.license)">{{imgInfo.license}}</a></li> - <li><a :href="imgInfo.url" class="underline">Source URL</a></li> - <li>Artist: {{imgInfo.artist}}</li> - <li v-if="imgInfo.credit != ''">Credit: {{imgInfo.credit}}</li> - </ul> - </div> - <div v-else> - <div v-if="tolNode.imgName[0] != null" :style="firstImgStyles"/> - <ul v-if="imgInfo1 != null"> - <li>Obtained via: {{imgInfo1.src}}</li> - <li>License: <a :href="licenseToUrl(imgInfo1.license)">{{imgInfo1.license}}</a></li> - <li><a :href="imgInfo1.url" class="underline">Source URL</a></li> - <li>Artist: {{imgInfo1.artist}}</li> - <li v-if="imgInfo1.credit != ''">Credit: {{imgInfo1.credit}}</li> - </ul> - <div v-if="tolNode.imgName[1] != null" :style="secondImgStyles"/> - <ul v-if="imgInfo2 != null"> - <li>Obtained via: {{imgInfo2.src}}</li> - <li>License: <a :href="licenseToUrl(imgInfo2.license)">{{imgInfo2.license}}</a></li> - <li><a :href="imgInfo2.url" class="underline">Source URL</a></li> - <li>Artist: {{imgInfo2.artist}}</li> - <li v-if="imgInfo2.credit != ''">Credit: {{imgInfo2.credit}}</li> - </ul> - </div> - </div> - <div v-if="descInfo == null && descInfo1 == null && descInfo2 == null"> - (No description found) - </div> - <div v-else-if="descInfo != null"> - <div> - Redirected: {{descInfo.fromRedirect}} <br/> - Short-description from {{descInfo.fromDbp ? 'DBpedia' : 'Wikipedia'}} <br/> - <a :href="'https://en.wikipedia.org/?curid=' + descInfo.wikiId" class="underline"> - Wikipedia Link - </a> - </div> - <hr/> - <div>{{descInfo.text}}</div> - </div> - <div v-else> - <div> - <h2 class="font-bold">{{subName1}}</h2> - <div v-if="descInfo1 != null">{{descInfo1.text}}</div> - <div v-else>(No description found)</div> - </div> - <div> - <h2 class="font-bold">{{subName2}}</h2> - <div v-if="descInfo2 != null">{{descInfo2.text}}</div> - <div v-else>(No description found)</div> + <div v-if="tolNode == null">Querying server</div> + <template v-else> + <div v-if="nodes.length > 1">This is a compound node</div> + <div v-for="idx in (nodes.length == 1 ? [0] : [0, 1])"> + <h1 v-if="nodes.length > 1" class="text-center font-bold"> + {{getDisplayName(subNames![idx], nodes[idx])}} + </h1> + <div class="flex gap-1"> + <div class="w-1/2"> + <div :style="getImgStyles(nodes[idx])"/> + <ul v-if="imgInfos[idx]! != null"> + <li>Obtained via: {{imgInfos[idx]!.src}}</li> + <li>License: + <a :href="licenseToUrl(imgInfos[idx]!.license)">{{imgInfos[idx]!.license}}</a> + </li> + <li><a :href="imgInfos[idx]!.url" class="underline">Source URL</a></li> + <li>Artist: {{imgInfos[idx]!.artist}}</li> + <li v-if="imgInfos[idx]!.credit != ''" class="overflow-auto"> + Credit: {{imgInfos[idx]!.credit}} + </li> + </ul> + </div> + <div v-if="descInfos[idx]! != null"> + <div> + Redirected: {{descInfos[idx]!.fromRedirect}} <br/> + Short-description from {{descInfos[idx]!.fromDbp ? 'DBpedia' : 'Wikipedia'}} <br/> + <a :href="'https://en.wikipedia.org/?curid=' + descInfos[idx]!.wikiId" class="underline"> + Wikipedia Link + </a> + </div> + <hr/> + <div>{{descInfos[idx]!.text}}</div> + </div> + <div v-else> + (No description found) + </div> </div> </div> - </div> - + </template> </div> </div> </template> diff --git a/src/layout.ts b/src/layout.ts index 15c156b..79d3b70 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -308,7 +308,7 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, let numChildren = node.children.length; let areaAR = newDims[0] / newDims[1]; // Aspect ratio let lowestEmpSpc = Number.POSITIVE_INFINITY, usedNumCols = 0, usedNumRows = 0, usedTileSz = 0; - const MAX_TRIES = 20; // If there are many possibilities, skip some + const MAX_TRIES = 50; // If there are many possibilities, skip some let ptlNumCols = numChildren == 1 ? [1] : linspace(1, numChildren, Math.min(numChildren, MAX_TRIES)).map(n => Math.floor(n)); for (let numCols of ptlNumCols){ @@ -48,11 +48,15 @@ export type ImgInfo = { license: string, artist: string, credit: string, -} -export type TileInfoResponse = { - tolNode: null | TolNode, - descData: null | DescInfo | [DescInfo, DescInfo], - imgData: null | ImgInfo | [ImgInfo, ImgInfo], +}; +export type NodeInfo = { + tolNode: TolNode, + descInfo: null | DescInfo, + imgInfo: null | ImgInfo, +}; +export type InfoResponse = { + nodeInfo: NodeInfo, + subNodesInfo: [] | [NodeInfo, NodeInfo], }; // Used by auto-mode and tutorial @@ -65,6 +69,7 @@ export type UiOptions = { // Shared coloring/sizing textColor: string, // CSS color bgColor: string, + bgColorAlt: string, bgColorLight: string, bgColorDark: string, bgColorLight2: string, |
