Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Out of range on sequence: show message on tooltip on click annotation #232

Open
wants to merge 28 commits into
base: feature/sync-sequence
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3610b34
Add message on tooltip for aligment
p3rcypj Apr 18, 2024
c6c0f68
Merge branch 'feature/sync-sequence' into feature/alignment
p3rcypj Dec 31, 2024
5bfebca
Change Gene viewer styles
p3rcypj Dec 31, 2024
b6042e0
Make BLAST not default. To be verified
p3rcypj Dec 31, 2024
6ea1b2f
Coverage
p3rcypj Dec 31, 2024
8fe040a
Fix loader to stop showing loading when it was still loading
p3rcypj Dec 31, 2024
9c2b11c
Chunk large uniprot protein requests. Display "can take a while"
p3rcypj Dec 31, 2024
a8ce9dd
Comment some hidden grid
p3rcypj Dec 31, 2024
fe03bc1
Create cache
p3rcypj Dec 31, 2024
7ba22a1
Refactor code
p3rcypj Jan 3, 2025
03666b2
Prettify
p3rcypj Jan 3, 2025
852756b
Fix lint warnings
p3rcypj Jan 7, 2025
bb2d879
Unnecessary optional title in LoaderMask
p3rcypj Jan 7, 2025
4bfabe9
Wrap args in obj
p3rcypj Jan 7, 2025
3bff218
Refactor coverage feature
p3rcypj Jan 7, 2025
e78d878
Move functions
p3rcypj Jan 7, 2025
7542466
i18n
p3rcypj Jan 7, 2025
9321825
Small refactor
p3rcypj Jan 7, 2025
98ad006
Abstract code into private methods
p3rcypj Jan 7, 2025
064b96b
Refactor protein mappings
p3rcypj Jan 7, 2025
743ae3c
Refactor chains
p3rcypj Jan 7, 2025
f6e112a
Add garbageCollector and change to localStorage
p3rcypj Jan 8, 2025
dd072ea
Add minor comment
p3rcypj Jan 19, 2025
14e1816
Merge branch 'feature/sync-sequence' into feature/alignment
p3rcypj Jan 27, 2025
15a65c2
Warn about quota exceeded
p3rcypj Jan 27, 2025
04da5c5
New pdbe-molstar version that takes into account uniprotId
p3rcypj Jan 27, 2025
4e422da
Typo
p3rcypj Jan 27, 2025
2ecf5e8
Fix tests
p3rcypj Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { RequestError, getFromUrl } from "../request-utils";
import { emdbsFromPdbUrl, getEmdbsFromMapping, PdbEmdbMapping } from "./mapping";
import { Maybe } from "../../utils/ts-utils";
import i18n from "../../domain/utils/i18n";
import { getSessionCache, setSessionCache } from "../session-cache";

export class BionotesPdbInfoRepository implements PdbInfoRepository {
get(pdbId: PdbId): FutureData<PdbInfo> {
get(pdbId: PdbId, canTakeAWhile: () => void): FutureData<PdbInfo> {
const proteinMappingUrl = `${routes.bionotes}/api/mappings/PDB/Uniprot/${pdbId}`;
const fallbackProteinMappingUrl = `${routes.ebi}/pdbe/api/mappings/uniprot/${pdbId}`;
const polymerCoverage = `${routes.ebi}/pdbe/api/pdb/entry/polymer_coverage/${pdbId}/`;
Expand Down Expand Up @@ -53,62 +54,81 @@ export class BionotesPdbInfoRepository implements PdbInfoRepository {
emdbMapping: emdbMapping$,
};

return Future.joinObj(data$).flatMap(data => {
const { uniprotMapping, fallbackProteinMapping, emdbMapping, molecules } = data;
return Future.joinObj(data$)
.flatMap(data => {
const { uniprotMapping, fallbackProteinMapping, emdbMapping, molecules } = data;

const hasProteinRes = uniprotMapping || fallbackProteinMapping;
const hasProteinRes = uniprotMapping || fallbackProteinMapping;

if (!hasProteinRes) console.debug(`Uniprot mapping not found for ${pdbId}`);
if (!hasProteinRes) console.debug(`Uniprot mapping not found for ${pdbId}`);

const chains = molecules
.flatMap(({ chains }) => chains)
.map(chain => ({
structAsymId: chain.struct_asym_id,
chainId: chain.chain_id,
const chains = molecules
.flatMap(({ chains }) => chains)
.map(chain => ({
structAsymId: chain.struct_asym_id,
chainId: chain.chain_id,
}));

const proteinsMappingChains =
(hasProteinRes &&
((uniprotMapping &&
this.bionotesProteinMapping(pdbId, uniprotMapping, chains)) ||
(fallbackProteinMapping &&
this.ebiProteinMapping(pdbId, fallbackProteinMapping, chains)))) ||
chains;

const emdbs = getEmdbsFromMapping(emdbMapping, pdbId).map(emdbId => ({
id: emdbId,
}));

const proteinsMappingChains =
(hasProteinRes &&
((uniprotMapping &&
this.bionotesProteinMapping(pdbId, uniprotMapping, chains)) ||
(fallbackProteinMapping &&
this.ebiProteinMapping(pdbId, fallbackProteinMapping, chains)))) ||
chains;

const emdbs = getEmdbsFromMapping(emdbMapping, pdbId).map(emdbId => ({ id: emdbId }));

const proteinsObj =
(uniprotMapping && uniprotMapping[pdbId.toLowerCase()]) ??
(fallbackProteinMapping && fallbackProteinMapping[pdbId.toLowerCase()]?.UniProt);

const proteins = proteinsObj && _(proteinsObj).keys().join(",");
const proteinsInfoUrl = `${routes.bionotes}/api/lengths/UniprotMulti/${proteins ?? ""}`;
const proteinsInfo$ = proteinsObj
? getFromUrl<ProteinsInfo>(proteinsInfoUrl)
: Future.success<ProteinsInfo, Error>({});

console.debug("Chains with proteins: ", proteinsMappingChains);

return proteinsInfo$.map(proteinsInfo => {
const proteins = _(proteinsInfo)
.toPairs()
.map(
([proteinId, proteinInfo]): Protein => {
const [_length, name, gen, organism] = proteinInfo;
return { id: proteinId, name, gen, organism };
}
)
.value();

return buildPdbInfo({
id: pdbId,
emdbs: emdbs,
ligands: [],
proteins,
chainsMappings: proteinsMappingChains,
const proteinsObj =
(uniprotMapping && uniprotMapping[pdbId.toLowerCase()]) ??
(fallbackProteinMapping &&
fallbackProteinMapping[pdbId.toLowerCase()]?.UniProt);

const proteinChunks = proteinsObj
? _(proteinsObj).keys().sort().chunk(4).value()
: [];

if (proteinChunks.length > 1 && !getSessionCache<{ proteinsInfo: boolean }>(pdbId)?.proteinsInfo)
setTimeout(canTakeAWhile, 2000);

const proteinInfoRequests = proteinChunks.map(chunk => {
const proteinsChunk = chunk.join(",");
const proteinsInfoUrlChunk = `${routes.bionotes}/api/lengths/UniprotMulti/${proteinsChunk}`;

return getFromUrl<ProteinsInfo>(proteinsInfoUrlChunk);
});

const proteinsInfo$: FutureData<ProteinsInfo> = Future.parallel(
proteinInfoRequests,
{ maxConcurrency: 2, }
).map(responses => Object.assign({}, ...responses)).tap(() => {
setSessionCache(pdbId, { proteinsInfo: true });
});

console.debug("Chains with proteins: ", proteinsMappingChains);

return proteinsInfo$.map(proteinsInfo => {
const proteins = _(proteinsInfo)
.toPairs()
.map(
([proteinId, proteinInfo]): Protein => {
const [_length, name, gen, organism] = proteinInfo;
return { id: proteinId, name, gen, organism };
}
)
.value();

return buildPdbInfo({
id: pdbId,
emdbs: emdbs,
ligands: [],
proteins,
chainsMappings: proteinsMappingChains,
});
});
});
});
}

private bionotesProteinMapping(
Expand Down
22 changes: 20 additions & 2 deletions app/assets/javascripts/3dbio_viewer/src/data/request-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { parseFromCodec } from "../utils/codec";
import { Future } from "../utils/future";
import { axiosRequest, defaultBuilder, RequestResult } from "../utils/future-axios";
import { Maybe } from "../utils/ts-utils";
import { getSessionCache, hashUrl, setSessionCache } from "./session-cache";

export type RequestError = { message: string };

const timeout = 20e3;
const timeout = 30e3;

export function getFromUrl<Data>(url: string): Future<RequestError, Data> {
return request<Data>({ method: "GET", url, timeout }).map(res => res.data);
Expand Down Expand Up @@ -80,5 +81,22 @@ function xmlToJs<Data>(xml: string): Future<RequestError, Data> {
export function request<Data>(
request: AxiosRequestConfig
): Future<RequestError, RequestResult<Data>> {
return axiosRequest(defaultBuilder, request);
if (!request.url) {
return axiosRequest(defaultBuilder, request);
}

const params = request.params
? JSON.stringify(request.params, Object.keys(request.params).sort())
p3rcypj marked this conversation as resolved.
Show resolved Hide resolved
: "";

const cacheKey = hashUrl(request.url + params);
const cachedResult = getSessionCache<RequestResult<Data>>(cacheKey);
if (cachedResult) return Future.success(cachedResult);

return axiosRequest<RequestError, Data>(defaultBuilder, request).map(result => {
if (result.response.status >= 200 && result.response.status < 300) {
setSessionCache(cacheKey, result);
}
return result;
});
}
34 changes: 34 additions & 0 deletions app/assets/javascripts/3dbio_viewer/src/data/session-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createHash } from "crypto";
import { Maybe } from "../utils/ts-utils";

const cacheExpires = 3.6e6;
p3rcypj marked this conversation as resolved.
Show resolved Hide resolved

export function hashUrl(url: string): string {
return createHash("sha256").update(url).digest("hex");
}

export function getSessionCache<Data>(key: string): Maybe<Data> {
const cached = sessionStorage.getItem(key);
if (!cached) return undefined;

const { value, timestamp } = JSON.parse(cached) as {
p3rcypj marked this conversation as resolved.
Show resolved Hide resolved
value: Data;
timestamp: number;
};

if (Date.now() - timestamp > cacheExpires) {
sessionStorage.removeItem(key);
return undefined;
}

return value;
}

export function setSessionCache<Data>(key: string, value: Data): void {
const cacheEntry = {
value,
timestamp: Date.now(),
};

sessionStorage.setItem(key, JSON.stringify(cacheEntry));
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function getFragmentToolsLink(options: {
}

export function isBlastFragment(subtrack: Subtrack, fragment: FragmentU): boolean {
return subtrack.isBlast !== false && fragment.end - fragment.start >= 3;
return Boolean(subtrack.isBlast) && fragment.end - fragment.start >= 3;
}

export function getBlastUrl(protein: string, subtrack: Subtrack, fragment: FragmentU): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function getTracksFromFragments(fragments: Fragments): Track[] {
labelTooltip: subtrack.description,
shape: subtrack.shape || "rectangle",
source: subtrack.source,
isBlast: subtrack.isBlast ?? true,
isBlast: subtrack.isBlast,
locations: slots.map(fragments => ({ fragments })),
subtype: subtrack.subtype,
overlapping: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { PdbId } from "../entities/Pdb";
import { PdbInfo } from "../entities/PdbInfo";

export interface PdbInfoRepository {
get(pdbId: PdbId): FutureData<PdbInfo>;
get(pdbId: PdbId, canTakeAWhile: () => void): FutureData<PdbInfo>;
p3rcypj marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { PdbInfoRepository } from "../repositories/PdbInfoRepository";
export class GetPdbInfoUseCase {
constructor(private pdbInfoRepository: PdbInfoRepository) {}

execute(pdbId: PdbId): FutureData<PdbInfo> {
return this.pdbInfoRepository.get(pdbId);
execute(pdbId: PdbId, canTakeAWhile: () => void): FutureData<PdbInfo> {
return this.pdbInfoRepository.get(pdbId, canTakeAWhile);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export type LoaderKey = keyof typeof loaderMessages;
const loaderMessages = {
readingSequence: [i18n.t("Reading sequence..."), 0],
getRelatedPdbModel: [i18n.t("Getting PDB related model..."), 0],
initPlugin: [i18n.t("Starting 3D Viewer..."), 1], //already loading PDB
pdbLoader: [i18n.t("Loading PDB Data..."), 1],
updateVisualPlugin: [i18n.t("Updating selection..."), 2],
pdbLoader: [i18n.t("Loading PDB Data..."), 3],
initPlugin: [i18n.t("Starting 3D Viewer..."), 3], //already loading PDB
uploadedModel: [i18n.t("Loading uploaded model..."), 2],
loadModel: [i18n.t("Loading model..."), 4], //PDB, EMDB, PDB-REDO, CSTF, CERES
exportAnnotations: [i18n.t("Retrieving all annotations..."), 5],
Expand All @@ -61,8 +61,9 @@ export const RootViewerContents: React.FC<RootViewerContentsProps> = React.memo(
const {
loading,
title,
setLoader,
updateLoaderStatus,
updateOnResolve: updateLoader,
updateOnResolve,
loaders,
resetLoaders,
} = useMultipleLoaders<LoaderKey>(loadersInitialState);
Expand All @@ -76,7 +77,17 @@ export const RootViewerContents: React.FC<RootViewerContentsProps> = React.memo(

const uploadData = getUploadData(externalData);

const { pdbInfoLoader, setLigands } = usePdbInfo(selection, uploadData);
const canTakeAWhile = React.useCallback(
() =>
setLoader("pdbLoader", {
status: "loading",
message: i18n.t("Loading PDB Data...\nThis can take several minutes to load."),
priority: 10,
}),
[setLoader]
);

const { pdbInfoLoader, setLigands } = usePdbInfo(selection, uploadData, canTakeAWhile);
const [pdbLoader, setPdbLoader] = usePdbLoader(selection, pdbInfoLoader);
const pdbInfo = pdbInfoLoader.type === "loaded" ? pdbInfoLoader.data : undefined;

Expand Down Expand Up @@ -114,12 +125,27 @@ export const RootViewerContents: React.FC<RootViewerContentsProps> = React.memo(
}, [uploadDataToken, networkToken, compositionRoot]);

const pdbId = React.useMemo(() => getMainItem(selection, "pdb"), [selection]);
const prevPdbId = React.useRef(pdbId);

const chainId = selection.chainId;
const prevChainId = React.useRef(chainId);

React.useEffect(() => {
if (pdbId && pdbId !== prevPdbId.current) resetLoaders(loadersInitialState);
}, [pdbId, prevPdbId, resetLoaders]);

React.useEffect(() => {
if (chainId && chainId !== prevChainId.current)
setLoader("pdbLoader", loadersInitialState.pdbLoader);
}, [chainId, pdbId, prevPdbId, resetLoaders, setLoader]);

React.useEffect(() => {
prevPdbId.current = pdbId;
}, [pdbId]);

React.useEffect(() => {
const init = loaders.initPlugin;
if (init.status !== "loaded") return;
resetLoaders({ ...loadersInitialState, initPlugin: init });
}, [pdbId, resetLoaders, loaders.initPlugin]);
prevChainId.current = chainId;
}, [chainId]);

React.useEffect(() => {
const critical = criticalLoaders.find(loader => loader.status === "error");
Expand Down Expand Up @@ -150,7 +176,7 @@ export const RootViewerContents: React.FC<RootViewerContentsProps> = React.memo(
onSelectionChange={setSelection}
onLigandsLoaded={setLigands}
proteinNetwork={proteinNetwork}
updateLoader={updateLoader}
updateLoader={updateOnResolve}
loaderBusy={loading}
/>
</div>
Expand All @@ -171,7 +197,7 @@ export const RootViewerContents: React.FC<RootViewerContentsProps> = React.memo(
pdbLoader={pdbLoader}
setPdbLoader={setPdbLoader}
toolbarExpanded={toolbarExpanded}
updateLoader={updateLoader}
updateLoader={updateOnResolve}
/>
</div>
</ResizableBox>
Expand Down
Loading
Loading