Skip to content
Snippets Groups Projects
Commit cbaaed64 authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc
Browse files

feat(overlays): implemented overlays initial functionality

parent 59d4fd60
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!78Resolve MIN-189 "/display general overlays"
Pipeline #83229 passed
Showing
with 364 additions and 144 deletions
......@@ -10,7 +10,11 @@ export const GeneralOverlays = (): JSX.Element => {
<p className="mb-5 text-sm font-semibold">General Overlays:</p>
<ul>
{generalPublicOverlays.map(overlay => (
<OverlayListItem key={overlay.idObject} name={overlay.name} />
<OverlayListItem
key={overlay.idObject}
name={overlay.name}
overlayId={overlay.idObject}
/>
))}
</ul>
</div>
......
......@@ -12,7 +12,7 @@ const renderComponent = (initialStoreState: InitialStoreState = {}): { store: St
return (
render(
<Wrapper>
<OverlayListItem name="Ageing brain" />
<OverlayListItem name="Ageing brain" overlayId={21} />
</Wrapper>,
),
{
......
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getOverlayBioEntityForAllModels } from '@/redux/overlayBioEntity/overlayBioEntity.thunk';
import { Button } from '@/shared/Button';
interface OverlayListItemProps {
name: string;
overlayId: number;
}
export const OverlayListItem = ({ name }: OverlayListItemProps): JSX.Element => {
const onViewOverlay = (): void => {};
export const OverlayListItem = ({ name, overlayId }: OverlayListItemProps): JSX.Element => {
const onDownloadOverlay = (): void => {};
const dispatch = useAppDispatch();
const onViewOverlay = (): void => {
dispatch(getOverlayBioEntityForAllModels({ overlayId }));
};
return (
<li className="flex flex-row flex-nowrap justify-between pl-5 [&:not(:last-of-type)]:mb-4">
......
import { ZERO } from '@/constants/common';
import { GetHex3ColorGradientColorWithAlpha } from '@/hooks/useTriColorLerp';
import { OverlayBioEntityRender } from '@/types/OLrendering';
import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
import Feature from 'ol/Feature';
import Polygon, { fromExtent } from 'ol/geom/Polygon';
import { Fill, Style } from 'ol/style';
export const getOverlayFeatures = (
bioEntities: OverlayBioEntityRender[],
pointToProjection: UsePointToProjectionResult,
getHex3ColorGradientColorWithAlpha: GetHex3ColorGradientColorWithAlpha,
): Feature<Polygon>[] =>
bioEntities.map(entity => {
const feature = new Feature({
geometry: fromExtent([
...pointToProjection({ x: entity.x1, y: entity.y1 }),
...pointToProjection({ x: entity.x2, y: entity.y2 }),
]),
});
feature.setStyle(
new Style({
fill: new Fill({ color: getHex3ColorGradientColorWithAlpha(entity.value || ZERO) }),
}),
);
return feature;
});
import Geometry from 'ol/geom/Geometry';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { useMemo } from 'react';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { useTriColorLerp } from '@/hooks/useTriColorLerp';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { overlayBioEntitiesForCurrentModelSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector';
import { getOverlayFeatures } from './getOverlayFeatures';
export const useOlMapOverlaysLayer = (): VectorLayer<VectorSource<Geometry>> => {
const pointToProjection = usePointToProjection();
const { getHex3ColorGradientColorWithAlpha } = useTriColorLerp();
const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector);
const features = useMemo(
() => getOverlayFeatures(bioEntities, pointToProjection, getHex3ColorGradientColorWithAlpha),
[bioEntities, getHex3ColorGradientColorWithAlpha, pointToProjection],
);
const vectorSource = useMemo(() => {
return new VectorSource({
features,
});
}, [features]);
const overlaysLayer = useMemo(
() =>
new VectorLayer({
source: vectorSource,
}),
[vectorSource],
);
return overlaysLayer;
};
......@@ -4,6 +4,7 @@ import { MapConfig, MapInstance } from '../../MapViewer.types';
import { useOlMapPinsLayer } from './pinsLayer/useOlMapPinsLayer';
import { useOlMapReactionsLayer } from './reactionsLayer/useOlMapReactionsLayer';
import { useOlMapTileLayer } from './useOlMapTileLayer';
import { useOlMapOverlaysLayer } from './overlaysLayer/useOlMapOverlaysLayer';
interface UseOlMapLayersInput {
mapInstance: MapInstance;
......@@ -13,14 +14,15 @@ export const useOlMapLayers = ({ mapInstance }: UseOlMapLayersInput): MapConfig[
const tileLayer = useOlMapTileLayer();
const pinsLayer = useOlMapPinsLayer();
const reactionsLayer = useOlMapReactionsLayer();
const overlaysLayer = useOlMapOverlaysLayer();
useEffect(() => {
if (!mapInstance) {
return;
}
mapInstance.setLayers([tileLayer, reactionsLayer, pinsLayer]);
}, [reactionsLayer, tileLayer, pinsLayer, mapInstance]);
mapInstance.setLayers([tileLayer, reactionsLayer, pinsLayer, overlaysLayer]);
}, [reactionsLayer, tileLayer, pinsLayer, mapInstance, overlaysLayer]);
return [tileLayer, pinsLayer, reactionsLayer];
return [tileLayer, pinsLayer, reactionsLayer, overlaysLayer];
};
// const overlayParticleMock = {
// left: {
// // molekuła na której jest prostokąt
// id: 38929,
// model: 52,
// glyph: null,
// submodel: null,
// compartment: 46645,
// elementId: 'path_0_sa8410',
// x: 15181.0, // wspolrzedne do lewego gornego naroznika
// y: 6976.0, // wspolrzedne do lewego gornego naroznika
// z: 4528,
// width: 80.0,
// height: 40.0,
// fontSize: 12.0,
// fontColor: {
// alpha: 255,
// rgb: -16777216,
// },
// fillColor: {
// alpha: 255,
// rgb: -3342388, // wartosc hexa przeksztalcona na wartosc binarna? rgb
// },
// borderColor: {
// alpha: 255,
// rgb: -16777216,
// },
// visibilityLevel: '3',
// transparencyLevel: '0',
// notes: '',
// symbol: 'ATP6V0B',
// fullName: 'ATPase H+ transporting V0 subunit b',
// abbreviation: null,
// formula: null,
// name: 'ATP6V0B',
// nameX: 15181.0,
// nameY: 6976.0,
// nameWidth: 80.0,
// nameHeight: 40.0,
// nameVerticalAlign: 'MIDDLE',
// nameHorizontalAlign: 'CENTER',
// synonyms: ['HATPL', 'VMA16'],
// formerSymbols: ['ATP6F'],
// activity: false,
// lineWidth: 1.0,
// complex: 38928,
// initialAmount: null,
// charge: null,
// initialConcentration: 0.0,
// onlySubstanceUnits: false,
// homodimer: 1,
// hypothetical: null,
// boundaryCondition: false,
// constant: false,
// modificationResidues: [],
// stringType: 'Protein',
// substanceUnits: null,
// references: [
// {
// link: 'https://www.ncbi.nlm.nih.gov/gene/533',
// type: 'ENTREZ',
// resource: '533',
// id: 178289,
// annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
// },
// {
// link: 'https://www.ncbi.nlm.nih.gov/protein/NM_004047',
// type: 'REFSEQ',
// resource: 'NM_004047',
// id: 178290,
// annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
// },
// {
// link: 'https://www.genenames.org/cgi-bin/gene_symbol_report?match=ATP6V0B',
// type: 'HGNC_SYMBOL',
// resource: 'ATP6V0B',
// id: 178291,
// annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
// },
// {
// link: 'https://www.ensembl.org/id/ENSG00000117410',
// type: 'ENSEMBL',
// resource: 'ENSG00000117410',
// id: 178292,
// annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
// },
// {
// link: 'https://purl.uniprot.org/uniprot/Q99437',
// type: 'UNIPROT',
// resource: 'Q99437',
// id: 178293,
// annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
// },
// {
// link: 'https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/861',
// type: 'HGNC',
// resource: '861',
// id: 178294,
// annotatorClassName: 'lcsb.mapviewer.annotation.services.annotators.HgncAnnotator',
// },
// ],
// },
// right: {
// // definiuje prostokat, na jakiej podstawie zostalo matchowane, moze byc nazwa, identyfikator, nazwa+mapa
// id: 9558,
// name: 'ATP6V0B',
// modelName: null,
// elementId: null,
// reverseReaction: null,
// lineWidth: null,
// // bede mial albo wartosc albo kolor, jak bede mial oba (raczej nie bedzie) to jest to bug
// value: -0.493438352, // musze wartość zmapowac na kolor -> -1 to np czerwony a 1 to niebieski. Nie mam koloru. W configuracji minervy jest legenda z kolorem dla -1, 0 i 1
// color: null,
// // kiedy nie ma wartości value i color to z configuracji jest brany element "no color val" dla koloru
// description: null,
// },
// };
// const color = {
// // definiuje prostokat, na jakiej podstawie zostalo matchowane, moze byc nazwa, identyfikator, nazwa+mapa
// id: 9558,
// name: 'ATP6V0B',
// modelName: null,
// elementId: null,
// reverseReaction: null,
// lineWidth: null,
// // bede mial albo wartosc albo kolor, jak bede mial oba (raczej nie bedzie) to jest to bug
// value: null,
// color: {
// alpha: 255,
// rgb: -16711936,
// },
// description: null,
// };
// // pole lines jest od reakcji - polegac na tym polu zeby odroznic molekuly od reakcji
// // porozmawiac z Markiem o sumowaniu overlays na przycisku "go to submap"
import {
maxColorValSelector,
minColorValSelector,
neutralColorValSelector,
overlayOpacitySelector,
} from '@/redux/configuration/configuration.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { getHexTricolorGradientColorWithAlpha } from '@/utils/convert/getHexTricolorGradientColorWithAlpha';
import { useCallback } from 'react';
export type GetHex3ColorGradientColorWithAlpha = (position: number) => string;
type UseTriColorLerpReturn = {
getHex3ColorGradientColorWithAlpha: GetHex3ColorGradientColorWithAlpha;
};
export const useTriColorLerp = (): UseTriColorLerpReturn => {
const minColorValHexString = useAppSelector(minColorValSelector) || '';
const maxColorValHexString = useAppSelector(maxColorValSelector) || '';
const neutralColorValHexString = useAppSelector(neutralColorValSelector) || '';
const overlayOpacityValue = useAppSelector(overlayOpacitySelector) || '';
const getHex3ColorGradientColorWithAlpha = useCallback(
(position: number) =>
getHexTricolorGradientColorWithAlpha({
leftColor: minColorValHexString,
middleColor: neutralColorValHexString,
rightColor: maxColorValHexString,
position,
alpha: Number(overlayOpacityValue),
}),
[minColorValHexString, neutralColorValHexString, maxColorValHexString, overlayOpacityValue],
);
return { getHex3ColorGradientColorWithAlpha };
};
import { z } from 'zod';
import { overlayLeftBioEntitySchema } from './overlayLeftBioEntitySchema';
import { overlayRightBioEntitySchema } from './overlayRightBioEntitySchema';
export const overlayBioEntitySchema = z.object({
left: overlayLeftBioEntitySchema,
right: overlayRightBioEntitySchema,
});
import { z } from 'zod';
import { colorSchema } from './colorSchema';
import { referenceSchema } from './referenceSchema';
export const overlayLeftBioEntitySchema = z.object({
id: z.number(),
model: z.number(),
glyph: z.unknown(),
submodel: z.unknown(),
compartment: z.number().nullable(),
elementId: z.union([z.string(), z.number()]),
x: z.number(),
y: z.number(),
z: z.number(),
width: z.number(),
height: z.number(),
fontSize: z.number().optional(),
fontColor: colorSchema.optional(),
fillColor: colorSchema.optional(),
borderColor: colorSchema,
visibilityLevel: z.string(),
transparencyLevel: z.string(),
notes: z.string(),
symbol: z.string().nullable(),
fullName: z.string().nullable().optional(),
abbreviation: z.unknown(),
formula: z.unknown(),
name: z.string(),
nameX: z.number().optional(),
nameY: z.number().optional(),
nameWidth: z.number().optional(),
nameHeight: z.number().optional(),
nameVerticalAlign: z.string().optional(),
nameHorizontalAlign: z.string().optional(),
synonyms: z.array(z.string()),
formerSymbols: z.array(z.string()).optional(),
activity: z.boolean().optional(),
lineWidth: z.number().optional(),
complex: z.number().nullable().optional(),
initialAmount: z.unknown().nullable(),
charge: z.unknown(),
initialConcentration: z.number().nullable().optional(),
onlySubstanceUnits: z.unknown(),
homodimer: z.number().optional(),
hypothetical: z.unknown(),
boundaryCondition: z.boolean().optional(),
constant: z.boolean().nullable().optional(),
modificationResidues: z.unknown(),
stringType: z.string(),
substanceUnits: z.boolean().nullable().optional(),
references: z.array(referenceSchema),
});
import { z } from 'zod';
import { colorSchema } from './colorSchema';
export const overlayRightBioEntitySchema = z.object({
id: z.number(),
name: z.string(),
modelName: z.boolean().nullable(),
elementId: z.string().nullable(),
reverseReaction: z.boolean().nullable(),
lineWidth: z.number().nullable().optional(),
value: z.number().nullable(),
color: colorSchema.nullable(),
description: z.string().nullable(),
});
......@@ -30,4 +30,6 @@ export const apiPath = {
`projects/${projectId}/backgrounds/`,
getProjectById: (projectId: string): string => `projects/${projectId}`,
getConfigurationOptions: (): string => 'configuration/options/',
getOverlayBioEntity: ({ overlayId, modelId }: { overlayId: number; modelId: number }): string =>
`projects/${PROJECT_ID}/overlays/${overlayId}/models/${modelId}/bioEntities/`,
};
export const MIN_COLOR_VAL_NAME_ID = 'MIN_COLOR_VAL';
export const MAX_COLOR_VAL_NAME_ID = 'MAX_COLOR_VAL';
export const SIMPLE_COLOR_VAL_NAME_ID = 'SIMPLE_COLOR_VAL';
export const NEUTRAL_COLOR_VAL_NAME_ID = 'NEUTRAL_COLOR_VAL';
export const OVERLAY_OPACITY_NAME_ID = 'OVERLAY_OPACITY';
import { createSelector } from '@reduxjs/toolkit';
import { configurationAdapter } from './configuration.adapter';
import { rootSelector } from '../root/root.selectors';
import {
MAX_COLOR_VAL_NAME_ID,
MIN_COLOR_VAL_NAME_ID,
NEUTRAL_COLOR_VAL_NAME_ID,
OVERLAY_OPACITY_NAME_ID,
} from './configuration.constants';
const configurationSelector = createSelector(rootSelector, state => state.configuration);
const configurationAdapterSelectors = configurationAdapter.getSelectors();
export const minColorValSelector = createSelector(
configurationSelector,
state => configurationAdapterSelectors.selectById(state, MIN_COLOR_VAL_NAME_ID)?.value,
);
export const maxColorValSelector = createSelector(
configurationSelector,
state => configurationAdapterSelectors.selectById(state, MAX_COLOR_VAL_NAME_ID)?.value,
);
export const neutralColorValSelector = createSelector(
configurationSelector,
state => configurationAdapterSelectors.selectById(state, NEUTRAL_COLOR_VAL_NAME_ID)?.value,
);
export const overlayOpacitySelector = createSelector(
configurationSelector,
state => configurationAdapterSelectors.selectById(state, OVERLAY_OPACITY_NAME_ID)?.value,
);
......@@ -13,6 +13,10 @@ export const currentModelSelector = createSelector(
(models, mapData) => models.find(model => model.idObject === mapData.modelId),
);
export const modelsIdsSelector = createSelector(modelsDataSelector, models =>
models.map(model => model.idObject),
);
export const currentModelIdSelector = createSelector(
currentModelSelector,
model => model?.idObject || MODEL_ID_DEFAULT,
......
import { OverlaysBioEntityState } from './overlayBioEntity.types';
export const OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK: OverlaysBioEntityState = {
overlaysId: [],
data: [],
};
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { getOverlayBioEntity, getOverlayBioEntityForAllModels } from './overlayBioEntity.thunk';
import { OverlaysBioEntityState } from './overlayBioEntity.types';
export const getOverlayBioEntityReducer = (
builder: ActionReducerMapBuilder<OverlaysBioEntityState>,
): void => {
builder.addCase(getOverlayBioEntity.fulfilled, (state, action) => {
if (action.payload) {
state.overlaysId = [action.meta.arg.overlayId];
state.data.push(...action.payload);
}
});
};
export const getOverlayBioEntityForAllModelsReducer = (
builder: ActionReducerMapBuilder<OverlaysBioEntityState>,
): void => {
builder.addCase(getOverlayBioEntityForAllModels.pending, state => {
state.data = [];
});
};
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '../root/root.selectors';
import { currentModelIdSelector } from '../models/models.selectors';
export const overlayBioEntitySelector = createSelector(
rootSelector,
state => state.overlayBioEntity,
);
export const overlayBioEntityDataSelector = createSelector(
overlayBioEntitySelector,
overlayBioEntity => overlayBioEntity.data,
);
export const overlayBioEntitiesForCurrentModelSelector = createSelector(
overlayBioEntityDataSelector,
currentModelIdSelector,
(data, currentModelId) => data.filter(entity => entity.modelId === currentModelId),
);
import { createSlice } from '@reduxjs/toolkit';
import {
getOverlayBioEntityForAllModelsReducer,
getOverlayBioEntityReducer,
} from './overlayBioEntity.reducers';
import { OverlaysBioEntityState } from './overlayBioEntity.types';
const initialState: OverlaysBioEntityState = {
overlaysId: [],
data: [],
};
export const overlayBioEntitySlice = createSlice({
name: 'overlayBioEntity',
initialState,
reducers: {},
extraReducers: builder => {
getOverlayBioEntityReducer(builder);
getOverlayBioEntityForAllModelsReducer(builder);
},
});
export default overlayBioEntitySlice.reducer;
import { createAsyncThunk } from '@reduxjs/toolkit';
import { z } from 'zod';
import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
import { OverlayBioEntity } from '@/types/models';
import { overlayBioEntitySchema } from '@/models/overlayBioEntitySchema';
import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
import { OverlayBioEntityRender } from '@/types/OLrendering';
import { parseOverlayBioEntityToOlRenderingFormat } from './overlayBioEntity.utils';
import { apiPath } from '../apiPath';
import { modelsIdsSelector } from '../models/models.selectors';
import type { RootState } from '../store';
type GetOverlayBioEntityThunkProps = {
overlayId: number;
modelId: number;
};
export const getOverlayBioEntity = createAsyncThunk(
'overlayBioEntity/getOverlayBioEntity',
async ({
overlayId,
modelId,
}: GetOverlayBioEntityThunkProps): Promise<OverlayBioEntityRender[] | undefined> => {
const response = await axiosInstanceNewAPI.get<OverlayBioEntity[]>(
apiPath.getOverlayBioEntity({ overlayId, modelId }),
);
const isDataValid = validateDataUsingZodSchema(response.data, z.array(overlayBioEntitySchema));
if (isDataValid) {
return parseOverlayBioEntityToOlRenderingFormat(response.data, overlayId);
}
return undefined;
},
);
type GetOverlayBioEntityForAllModelsThunkProps = { overlayId: number };
export const getOverlayBioEntityForAllModels = createAsyncThunk<
void,
GetOverlayBioEntityForAllModelsThunkProps,
{ state: RootState }
>(
'overlayBioEntity/getOverlayBioEntityForAllModels',
async ({ overlayId }, { dispatch, getState }): Promise<void> => {
const state = getState();
const modelsIds = modelsIdsSelector(state);
const asyncGetOverlayBioEntityFunctions = modelsIds.map(id =>
dispatch(getOverlayBioEntity({ overlayId, modelId: id })),
);
await Promise.all(asyncGetOverlayBioEntityFunctions);
},
);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment