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 23 commits into
base: feature/sync-sequence
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
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