From 19984e11e3bfd793188fd84c621a304330f4a303 Mon Sep 17 00:00:00 2001 From: Terry Truong Date: Mon, 23 May 2022 07:02:16 +1000 Subject: Display 'compound images' Adjust genLinkedImgs.py to associate multiple images to compound-nodes. Adjust server to send multiple image filenames for such nodes. Adjust Tile and TileInfoModal to display those compound-images. --- backend/data/README.md | 2 +- backend/data/genLinkedImgs.py | 55 ++++++++++++++++++++++---- backend/server.py | 60 +++++++++++++++++++---------- src/App.vue | 3 +- src/components/AncestryBar.vue | 2 +- src/components/Tile.vue | 83 ++++++++++++++++++++++++++++++++-------- src/components/TileInfoModal.vue | 80 +++++++++++++++++++++++++------------- src/tol.ts | 2 +- 8 files changed, 213 insertions(+), 74 deletions(-) diff --git a/backend/data/README.md b/backend/data/README.md index 438188c..a1bc287 100644 --- a/backend/data/README.md +++ b/backend/data/README.md @@ -39,7 +39,7 @@ data.db Tables - names: name TEXT, alt\_name TEXT, pref\_alt INT, PRIMARY KEY(name, alt\_name) - eol\_ids: id INT PRIMARY KEY, name TEXT - images: eol\_id INT PRIMARY KEY, source\_url TEXT, license TEXT, copyright\_owner TEXT -- linked\_imgs: name TEXT PRIMARY KEY, eol\_id INT +- linked\_imgs: name TEXT PRIMARY KEY, eol\_id INT, eol\_id2 INT - descs: name TEXT PRIMARY KEY, desc TEXT, redirected INT, wiki\_id INT, from\_dbp INT - r\_nodes: name TEXT PRIMARY KEY, tips INT - r\_edges: node TEXT, child TEXT, p\_support INT, PRIMARY KEY (node, child) diff --git a/backend/data/genLinkedImgs.py b/backend/data/genLinkedImgs.py index 5f49ffc..d32196e 100755 --- a/backend/data/genLinkedImgs.py +++ b/backend/data/genLinkedImgs.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -import sys +import sys, re import sqlite3 usageInfo = f"usage: {sys.argv[0]}\n" @@ -11,11 +11,14 @@ if len(sys.argv) > 1: sys.exit(1) dbFile = "data.db" +compoundNameRegex = re.compile(r"\[(.+) \+ (.+)]") +upPropagateCompoundImgs = False # Open db dbCon = sqlite3.connect(dbFile) dbCur = dbCon.cursor() -dbCur.execute("CREATE TABLE linked_imgs (name TEXT PRIMARY KEY, eol_id INT)") +dbCur.execute("CREATE TABLE linked_imgs (name TEXT PRIMARY KEY, eol_id INT, eol_id2 INT)") + # The eol_id2 column is used to provide the second part of a compound-image # Get nodes with images print("Getting nodes with images") resolvedNodes = {} # Will map node names to eol IDs with a usable image @@ -28,7 +31,8 @@ print("Got {} nodes".format(len(resolvedNodes))) # Iterate through resolved nodes, resolving ancestors where able print("Resolving ancestor nodes") nodesToResolve = {} -processedNodes = set() +processedNodes = {} +parentToChosenTips = {} iterNum = 0 while len(resolvedNodes) > 0: iterNum += 1 @@ -36,7 +40,7 @@ while len(resolvedNodes) > 0: print("At iteration {}".format(iterNum)) # Get next node (nodeName, eolId) = resolvedNodes.popitem() - processedNodes.add(nodeName) + processedNodes[nodeName] = eolId # Traverse upwards, resolving ancestors if able while True: # Get parent @@ -56,9 +60,10 @@ while len(resolvedNodes) > 0: # Check if highest-tips child if (childObjs[0]["name"] == nodeName): # Resolve parent, and continue from it - dbCur.execute("INSERT INTO linked_imgs VALUES (?, ?)", (parent, eolId)) + dbCur.execute("INSERT INTO linked_imgs VALUES (?, ?, ?)", (parent, eolId, None)) del nodesToResolve[parent] - processedNodes.add(parent) + processedNodes[parent] = eolId + parentToChosenTips[parent] = childObjs[0]["tips"] nodeName = parent continue else: @@ -71,8 +76,44 @@ while len(resolvedNodes) > 0: for (name, childObjs) in nodesToResolve.items(): childObj = next(c for c in childObjs if c["eolId"] != None) resolvedNodes[name] = childObj["eolId"] - dbCur.execute("INSERT INTO linked_imgs VALUES (?, ?)", (name, childObj["eolId"])) + parentToChosenTips[name] = childObj["tips"] + dbCur.execute("INSERT INTO linked_imgs VALUES (?, ?, ?)", (name, childObj["eolId"], None)) nodesToResolve.clear() +# Iterate through processed nodes with compound names +print("Replacing images for compound-name nodes") +iterNum = 0 +for nodeName in processedNodes.keys(): + iterNum += 1 + if iterNum % 1e3 == 0: + print("At iteration {}".format(iterNum)) + # + match = compoundNameRegex.fullmatch(nodeName) + if match != None: + # Replace associated image with subname images + (subName1, subName2) = match.group(1,2) + eolIdPair = [0, 0] + if subName1 in processedNodes: + eolIdPair[0] = processedNodes[subName1] + if subName2 in processedNodes: + eolIdPair[1] = processedNodes[subName2] + dbCur.execute("UPDATE linked_imgs SET eol_id = ?, eol_id2 = ? WHERE name = ?", + (eolIdPair[0], eolIdPair[1], nodeName,)) + if upPropagateCompoundImgs: + # Repeat operation for parents, where needed + while True: + # Get parent + row = dbCur.execute("SELECT node FROM edges WHERE child = ?", (nodeName,)).fetchone() + if row != None: + parent = row[0] + # Check num tips + (numTips,) = dbCur.execute("SELECT tips from nodes WHERE name = ?", (nodeName,)).fetchone() + if parent in parentToChosenTips and parentToChosenTips[parent] <= numTips: + # Replace associated image + dbCur.execute("UPDATE linked_imgs SET eol_id = ?, eol_id2 = ? WHERE name = ?", + (eolIdPair[0], eolIdPair[1], parent)) + nodeName = parent + continue + break # Close db dbCon.commit() dbCon.close() diff --git a/backend/server.py b/backend/server.py index f15c95f..5152503 100755 --- a/backend/server.py +++ b/backend/server.py @@ -16,9 +16,11 @@ usageInfo += "Starts a server that listens for GET requests to http://" + hostna usageInfo += "Responds to path+query /data/type1?name=name1 with JSON data.\n" usageInfo += "An additional query parameter tree=reduced is usable to get reduced-tree data\n" usageInfo += "\n" -usageInfo += "If type1 is 'node': Responds with map from names to objects representing node name1 and it's children.\n" +usageInfo += "If type1 is 'node': Responds with map from names to TolNode objects for node name1 and it's children.\n" usageInfo += "If type1 is 'chain': Like 'node', but gets nodes from name1 up to the root, and their direct children.\n" -usageInfo += "If type1 is 'search': Responds with a tolnode name that has alt-name name1, or null.\n" +usageInfo += "If type1 is 'search': Responds with a SearchSuggResponse object.\n" +usageInfo += "If type1 is 'info': Responds with a TileInfoResponse object.\n" +usageInfo += "(Object type information can be found in src/)\n" if len(sys.argv) > 1: print(usageInfo, file=sys.stderr) sys.exit(1) @@ -59,9 +61,16 @@ def lookupNodes(names, useReducedTree): nodeObjs[name]["imgName"] = str(eolId) + ".jpg" # Get 'linked' images for unresolved names unresolvedNames = [n for n in nodeObjs if nodeObjs[n]["imgName"] == None] - query = "SELECT name, eol_id from linked_imgs WHERE name IN ({})".format(",".join(["?"] * len(unresolvedNames))) - for (name, eolId) in cur.execute(query, unresolvedNames): - nodeObjs[name]["imgName"] = str(eolId) + ".jpg" + query = "SELECT name, eol_id, eol_id2 from linked_imgs WHERE name IN ({})" + query = query.format(",".join(["?"] * len(unresolvedNames))) + for (name, eolId, eolId2) in cur.execute(query, unresolvedNames): + if eolId2 == None: + nodeObjs[name]["imgName"] = str(eolId) + ".jpg" + else: + nodeObjs[name]["imgName"] = [ + str(eolId) + ".jpg" if eolId != 0 else None, + str(eolId2) + ".jpg" if eolId2 != 0 else None, + ] # Get preferred-name info query = "SELECT name, alt_name FROM names WHERE pref_alt = 1 AND name IN ({})".format(queryParamStr) for (name, altName) in cur.execute(query, names): @@ -106,30 +115,41 @@ def lookupNodeInfo(name, useReducedTree): nodeObj = temp[name] if name in temp else None # Get node desc descData = None - query = "SELECT desc, redirected, wiki_id, from_dbp from descs WHERE descs.name = ?" match = re.fullmatch(r"\[(.+) \+ (.+)]", name) if match == None: + query = "SELECT desc, redirected, wiki_id, from_dbp from descs WHERE descs.name = ?" row = cur.execute(query, (name,)).fetchone() if row != None: descData = {"text": row[0], "fromRedirect": row[1] == 1, "wikiId": row[2], "fromDbp": row[3] == 1} else: - # Get descs for compound-node elements + # Get descs for compound-node element descData = [None, None] - row = cur.execute(query, (match.group(1),)).fetchone() - if row != None: - descData[0] = {"text": row[0], "fromRedirect": row[1] == 1, "wikiId": row[2], "fromDbp": row[3] == 1} - row = cur.execute(query, (match.group(2),)).fetchone() - if row != None: - descData[1] = {"text": row[0], "fromRedirect": row[1] == 1, "wikiId": row[2], "fromDbp": row[3] == 1} + query = "SELECT name, desc, redirected, wiki_id, from_dbp from descs WHERE descs.name IN (?, ?)" + for row in cur.execute(query, match.group(1,2)): + if row[0] == match.group(1): + descData[0] = {"text": row[1], "fromRedirect": row[2] == 1, "wikiId": row[3], "fromDbp": row[4] == 1} + else: + descData[1] = {"text": row[1], "fromRedirect": row[2] == 1, "wikiId": row[3], "fromDbp": row[4] == 1} # Get img info - imgInfo = None - if nodeObj != None and nodeObj["imgName"] != None: - eolId = int(nodeObj["imgName"][:-4]) # Convert filename excluding .jpg suffix - imgInfoQuery = "SELECT eol_id, source_url, license, copyright_owner FROM images WHERE eol_id = ?" - row = cur.execute(imgInfoQuery, (eolId,)).fetchone() - imgInfo = {"eolId": row[0], "sourceUrl": row[1], "license": row[2], "copyrightOwner": row[3]} + imgData = None + if nodeObj != None: + if isinstance(nodeObj["imgName"], str): + eolId = int(nodeObj["imgName"][:-4]) # Convert filename excluding .jpg suffix + query = "SELECT eol_id, source_url, license, copyright_owner FROM images WHERE eol_id = ?" + row = cur.execute(query, (eolId,)).fetchone() + imgData = {"eolId": row[0], "sourceUrl": row[1], "license": row[2], "copyrightOwner": row[3]} + elif isinstance(nodeObj["imgName"], list): + # Get info for compound-image parts + imgData = [None, None] + idsToLookup = [int(n[:-4]) for n in nodeObj["imgName"] if n != None] + query = "SELECT eol_id, source_url, license, copyright_owner FROM images WHERE eol_id IN (?, ?)" + for row in cur.execute(query, idsToLookup): + if str(row[0]) == nodeObj["imgName"][0][:-4]: + imgData[0] = {"eolId": row[0], "sourceUrl": row[1], "license": row[2], "copyrightOwner": row[3]} + else: + imgData[1] = {"eolId": row[0], "sourceUrl": row[1], "license": row[2], "copyrightOwner": row[3]} # - return {"descData": descData, "imgInfo": imgInfo, "nodeObj": nodeObj} + return {"descData": descData, "imgData": imgData, "nodeObj": nodeObj} class DbServer(BaseHTTPRequestHandler): def do_GET(self): diff --git a/src/App.vue b/src/App.vue index e1da1f2..e26de01 100644 --- a/src/App.vue +++ b/src/App.vue @@ -638,7 +638,8 @@ export default defineComponent({ @search-close="searchOpen = false" @search-node="onSearchNode" @info-icon-click="onInfoIconClick"/> - diff --git a/src/components/AncestryBar.vue b/src/components/AncestryBar.vue index ca865e9..5419f78 100644 --- a/src/components/AncestryBar.vue +++ b/src/components/AncestryBar.vue @@ -71,7 +71,7 @@ export default defineComponent({ onTileClick(node: LayoutNode){ this.$emit('detached-ancestor-click', node); }, - onInfoIconClick(data: LayoutNode){ + onInfoIconClick(data: string){ this.$emit('info-icon-click', data); } }, diff --git a/src/components/Tile.vue b/src/components/Tile.vue index 0a404e6..7f15f3c 100644 --- a/src/components/Tile.vue +++ b/src/components/Tile.vue @@ -86,6 +86,9 @@ export default defineComponent({ } return capitalizeWords(this.tolNode.commonName || this.layoutNode.name); }, + hasCompoundImage(): boolean { + return Array.isArray(this.tolNode.imgName); + }, isOverflownRoot(): boolean { return this.overflownDim > 0 && !this.layoutNode.hidden && this.layoutNode.children.length > 0; }, @@ -147,16 +150,30 @@ export default defineComponent({ return layoutStyles; }, leafStyles(): Record { - return { - // Image (and scrims) - backgroundImage: this.tolNode.imgName != null ? - 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.4) 100%),' + - 'url(\'/img/' + this.tolNode.imgName.replaceAll('\'', '\\\'') + '\')' : - 'none', - backgroundColor: '#1c1917', - backgroundSize: 'cover', - borderRadius: 'inherit', - }; + if (!this.hasCompoundImage){ + return { + // Image (and scrims) + backgroundImage: this.tolNode.imgName != null ? + 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.4) 100%),' + + 'url(\'/img/' + (this.tolNode.imgName as string).replaceAll('\'', '\\\'') + '\')' : + 'none', + backgroundColor: '#1c1917', + backgroundSize: 'cover', + // Child layout + display: 'flex', + flexDirection: 'column', + // Other + borderRadius: 'inherit', + }; + } else { + return { + // Child layout + display: 'grid', + gridTemplateColumns: '1fr', + // Other + borderRadius: 'inherit', + }; + } }, leafHeaderStyles(): Record { let numChildren = this.tolNode.children.length; @@ -181,6 +198,12 @@ export default defineComponent({ whiteSpace: 'nowrap', }; }, + leafFirstImgStyles(): Record { + return this.leafSubImgStyles(0); + }, + leafSecondImgStyles(): Record { + return this.leafSubImgStyles(1); + }, nonleafStyles(): Record { let styles = { position: 'static', @@ -237,6 +260,8 @@ export default defineComponent({ minWidth: size, minHeight: size, margin: this.uiOpts.infoIconMargin + 'px', + marginTop: 'auto', + marginLeft: 'auto', }; }, sepSweptAreaStyles(): Record { @@ -312,7 +337,7 @@ export default defineComponent({ // For scrolling to a focused child if overflownRoot hasFocusedChild(newVal, oldVal){ if (newVal && this.isOverflownRoot){ - let focusedChild = this.layoutNode.children.find(n => n.hasFocus) + let focusedChild = this.layoutNode.children.find(n => n.hasFocus)! let bottomY = focusedChild.pos[1] + focusedChild.dims[1] + this.lytOpts.tileSpacing; let scrollTop = Math.max(0, bottomY - (this.overflownDim / 2)); // 'scrollTop' won't go over max this.$el.scrollTop = scrollTop; @@ -390,6 +415,21 @@ export default defineComponent({ } }, // Other + leafSubImgStyles(idx: number): Record { + return { + width: '100%', + height: '100%', + // Image (and scrims) + backgroundImage: (this.tolNode.imgName![idx]! != null) ? + 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.4) 100%),' + + 'url(\'/img/' + this.tolNode.imgName![idx]!.replaceAll('\'', '\\\'') + '\')' : + 'none', + backgroundColor: '#1c1917', + backgroundSize: 'cover', + borderRadius: 'inherit', + clipPath: idx == 0 ? 'polygon(0 0, 100% 0, 0 100%)' : 'polygon(100% 0, 0 100%, 100% 100%)', + }; + }, onTransitionEnd(evt: Event){ if (this.inTransition){ this.inTransition = false; @@ -411,13 +451,22 @@ export default defineComponent({