Skip to content

Commit

Permalink
Merge pull request #173 from xnought/thumbnails
Browse files Browse the repository at this point in the history
feat: preview thumbnails on protein
  • Loading branch information
ansengarvin authored Feb 15, 2024
2 parents 2603896 + 799ff8a commit 96e679e
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 50 deletions.
1 change: 1 addition & 0 deletions backend/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ CREATE TABLE proteins (
content bytea, -- stored markdown for the protein article (TODO: consider having a limit to how big this can be)
refs bytea, -- bibtex references mentioned in the content/article
species_id integer NOT NULL,
thumbnail bytea, -- thumbnail image of the protein in base64 format
FOREIGN KEY (species_id) REFERENCES species(id) ON UPDATE CASCADE ON DELETE CASCADE
);

Expand Down
29 changes: 27 additions & 2 deletions backend/src/api/protein.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from Bio.SeqUtils import molecular_weight, seq1
from ..db import Database, bytea_to_str, str_to_bytea

from ..api_types import ProteinEntry, UploadBody, UploadError, EditBody
from ..api_types import ProteinEntry, UploadBody, UploadError, EditBody, CamelModel
from io import BytesIO
from fastapi import APIRouter
from fastapi.responses import FileResponse, StreamingResponse
Expand Down Expand Up @@ -88,6 +88,16 @@ def pdb_to_fasta(pdb: PDB):
return ">{}\n{}".format(pdb.name, "".join(pdb.amino_acids()))


"""
ENDPOINTS TODO: add the other protein types here instead of in api_types.py
"""


class UploadPNGBody(CamelModel):
protein_name: str
base64_encoding: str


@router.get("/protein/pdb/{protein_name:str}")
def get_pdb_file(protein_name: str):
if protein_name_found(protein_name):
Expand Down Expand Up @@ -121,7 +131,8 @@ def get_protein_entry(protein_name: str):
proteins.mass,
proteins.content,
proteins.refs,
species.name
species.name,
proteins.thumbnail
FROM proteins
JOIN species ON species.id = proteins.species_id
WHERE proteins.name = %s;"""
Expand All @@ -140,13 +151,16 @@ def get_protein_entry(protein_name: str):
content,
refs,
species_name,
thumbnail,
) = only_returned_entry

# if byte arrays are present, decode them into a string
if content is not None:
content = bytea_to_str(content)
if refs is not None:
refs = bytea_to_str(refs)
if thumbnail is not None:
thumbnail = bytea_to_str(thumbnail)

return ProteinEntry(
name=name,
Expand All @@ -156,6 +170,7 @@ def get_protein_entry(protein_name: str):
content=content,
refs=refs,
species_name=species_name,
thumbnail=thumbnail,
)

except Exception as e:
Expand All @@ -180,6 +195,16 @@ def delete_protein_entry(protein_name: str):
log.error(e)


@router.post("/protein/upload/png", response_model=None)
def upload_protein_png(body: UploadPNGBody):
with Database() as db:
try:
query = """UPDATE proteins SET thumbnail = %s WHERE name = %s"""
db.execute(query, [str_to_bytea(body.base64_encoding), body.protein_name])
except Exception as e:
log.error(e)


# None return means success
@router.post("/protein/upload", response_model=UploadError | None)
def upload_protein_entry(body: UploadBody):
Expand Down
10 changes: 7 additions & 3 deletions backend/src/api/search.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter
from fastapi.exceptions import HTTPException
import logging as log
from ..db import Database
from ..db import Database, bytea_to_str
from ..api_types import CamelModel, ProteinEntry

router = APIRouter()
Expand Down Expand Up @@ -76,7 +76,8 @@ def search_proteins(body: SearchProteinsBody):
proteins.description,
proteins.length,
proteins.mass,
species.name
species.name,
proteins.thumbnail
FROM proteins
JOIN species ON species.id = proteins.species_id
WHERE proteins.name ILIKE %s"""
Expand All @@ -95,9 +96,12 @@ def search_proteins(body: SearchProteinsBody):
length=length,
mass=mass,
species_name=species_name,
thumbnail=bytea_to_str(thumbnail_bytes)
if thumbnail_bytes is not None
else None,
description=description,
)
for name, description, length, mass, species_name in entries_result
for name, description, length, mass, species_name, thumbnail_bytes in entries_result
],
total_found=len(entries_result),
)
Expand Down
1 change: 1 addition & 0 deletions backend/src/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ProteinEntry(CamelModel):
species_name: str
content: str | None = None
refs: str | None = None
thumbnail: str | None = None
description: str | None = None


Expand Down
2 changes: 2 additions & 0 deletions frontend/src/Router.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Error from "./routes/Error.svelte";
import Edit from "./routes/Edit.svelte";
import Tutorials from "./routes/Tutorials.svelte";
import ForceUploadThumbnails from "./routes/ForceUploadThumbnails.svelte";
</script>

<Router>
Expand All @@ -25,6 +26,7 @@
><Protein urlId={params.id} /></Route
>
<Route path="/edit/:id" let:params><Edit urlId={params.id} /></Route>
<Route path="/force-upload-thumbnails"><ForceUploadThumbnails /></Route>
<Route path="/*"><Error /></Route>
</main>
</Router>
7 changes: 5 additions & 2 deletions frontend/src/lib/ListProteins.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { navigate } from "svelte-routing";
import type { ProteinEntry } from "./backend";
import { numberWithCommas } from "./format";
import dummy from "../images/dummy.png";
export let allEntries: ProteinEntry[] | null = null;
</script>
Expand All @@ -18,7 +17,11 @@
title={`Name:${entry.name}\nDescription:${entry.description}`}
>
<div class="prot-thumb mr-2">
<img class="prot-thumb" src={dummy} alt="dummy" />
<img
class="prot-thumb"
src={entry.thumbnail ?? ""}
alt="thumbnail"
/>
</div>
<div class="prot-info">
<div class="prot-name">
Expand Down
99 changes: 65 additions & 34 deletions frontend/src/lib/ProteinVis.svelte
Original file line number Diff line number Diff line change
@@ -1,48 +1,79 @@
<script lang="ts">
import { onMount } from "svelte";
import { PDBeMolstarPlugin } from "../../venome-molstar";
import type { InitParams } from "../../venome-molstar/lib/spec";
import { Backend } from "./backend";
import { createEventDispatcher } from "svelte";
export let url =
"http://localhost:8000/data/pdbAlphaFold/Gh_comp271_c0_seq1.pdb";
const dispatch = createEventDispatcher<{
mount: { screenshot: typeof screenshot };
}>();
export let proteinName = "Gh_comp271_c0_seq1";
export let format = "pdb";
export let width = 500;
export let height = 500;
onMount(async () => {
//Create plugin instance
// @ts-ignore
var viewerInstance = new PDBeMolstarPlugin(); // loaded through app.html
//Set options (Checkout available options list in the documentation)
var options = {
customData: {
url,
format,
},
subscribeEvents: false,
bgColor: {
r: 255,
g: 255,
b: 255,
},
selectInteraction: true,
alphafoldView: true,
reactive: true,
sequencePanel: true,
hideControls: true,
hideCanvasControls: ["animation"],
};
//Get element from HTML/Template to place the viewer
var viewerContainer = document.getElementById("myViewer");
const url = `http://localhost:8000/protein/pdb/${proteinName}`;
const m = new PDBeMolstarPlugin(); // loaded through app.html
let divEl: HTMLDivElement;
const options: Partial<InitParams> = {
customData: {
url,
format,
binary: false,
},
subscribeEvents: false,
bgColor: {
r: 255,
g: 255,
b: 255,
},
selectInteraction: true,
alphafoldView: true,
reactive: true,
sequencePanel: true,
hideControls: true,
hideCanvasControls: ["animation"],
};
//Call render method to display the 3D view
// @ts-ignore
viewerInstance.render(viewerContainer, options);
/**
* @todo: don't upload the protein thumbnail everytime, just do once!
*/
onMount(async () => {
await m.render(divEl, options);
dispatch("mount", { screenshot });
});
async function screenshot(delayMs = 200) {
async function onLoad(funcToExec: () => void) {
return new Promise((resolve) => {
m.events.loadComplete.subscribe(() => {
funcToExec();
resolve(true);
});
});
}
async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
let p = "";
await onLoad(() => {
console.log("protein loaded");
});
await delay(delayMs); // why the fuck do I need this for the next line to work?
p = m.plugin.helpers.viewportScreenshot
?.getPreview()!
.canvas.toDataURL()!;
return p;
}
</script>

<div id="myViewer" style="width: {width}px; height: {height}px;" />
<div
bind:this={divEl}
id="myViewer"
style="width: {width}px; height: {height}px;"
/>

<style>
/* https://embed.plnkr.co/plunk/WlRx73uuGA9EJbpn */
Expand All @@ -52,6 +83,6 @@
#myViewer {
float: left;
position: relative;
z-index: 999;
z-index: 997;
}
</style>
1 change: 1 addition & 0 deletions frontend/src/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type { SearchProteinsResults } from './models/SearchProteinsResults';
export type { Tutorial } from './models/Tutorial';
export type { UploadBody } from './models/UploadBody';
export { UploadError } from './models/UploadError';
export type { UploadPNGBody } from './models/UploadPNGBody';
export type { ValidationError } from './models/ValidationError';

export { DefaultService } from './services/DefaultService';
16 changes: 8 additions & 8 deletions frontend/src/lib/openapi/models/ProteinEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
/* tslint:disable */
/* eslint-disable */
export type ProteinEntry = {
name: string;
length: number;
mass: number;
speciesName: string;
content?: (string | null);
refs?: (string | null);
description?: (string | null);
name: string;
length: number;
mass: number;
speciesName: string;
content?: string | null;
refs?: string | null;
thumbnail?: string | null;
description?: string | null;
};

9 changes: 9 additions & 0 deletions frontend/src/lib/openapi/models/UploadPNGBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UploadPNGBody = {
proteinName: string;
base64Encoding: string;
};

20 changes: 20 additions & 0 deletions frontend/src/lib/openapi/services/DefaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { SearchProteinsResults } from '../models/SearchProteinsResults';
import type { Tutorial } from '../models/Tutorial';
import type { UploadBody } from '../models/UploadBody';
import type { UploadError } from '../models/UploadError';
import type { UploadPNGBody } from '../models/UploadPNGBody';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
Expand Down Expand Up @@ -168,6 +169,25 @@ export class DefaultService {
},
});
}
/**
* Upload Protein Png
* @param requestBody
* @returns any Successful Response
* @throws ApiError
*/
public static uploadProteinPng(
requestBody: UploadPNGBody,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/protein/upload/png',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Upload Protein Entry
* @param requestBody
Expand Down
Loading

0 comments on commit 96e679e

Please sign in to comment.