diff --git a/docs/plugins/data/bioentities.md b/docs/plugins/data/bioentities.md index e17f46d1e1a4f32acfaa41a877fb8b4768514e9b..e6d8fd8c611ea2997be3d141025ca60821a74268 100644 --- a/docs/plugins/data/bioentities.md +++ b/docs/plugins/data/bioentities.md @@ -1,6 +1,6 @@ ### Data / BioEntities -The methods contained within 'Data / BioEntities' are used to access/modify data on content/drugs/chemicals entities, as well as pin and surface markers. +The methods contained within 'Data / BioEntities' are used to access/modify data on content/drugs/chemicals entities, as well as pin, surface and line markers. Below is a description of the methods, as well as the types they return. A description of the object types can be found in folder `/docs/types/`. @@ -49,27 +49,31 @@ Below is a description of the methods, as well as the types they return. A descr - **object:** ``` { - type: 'pin' OR 'surface' + type: 'pin' OR 'surface' OR 'line' id: string [optional] color: string opacity: number - x: number - y: number + x: number [optional] + y: number [optional] width: number [optional] height: number [optional] number: number [optional] modelId: number [optional] + start: { x: number; y: number } [optional] + end: { x: number; y: number } [optional] } ``` - **id** - optional, if not provided uuidv4 is generated - **color** - should be provided in hex format with hash (example: `#FF0000`) - **opacity** - should be a float between `0` and `1` (example: `0.54`) - - **x** - x coord on the map - - **y** - y coord on the map + - **x** - x coord on the map [surface/pin marker only] + - **y** - y coord on the map [surface/pin marker only] - **width** - width of surface [surface marker only] - **height** - width of height [surface marker only] - **number** - number presented on the pin [pin marker only] - **modelId** - if marker should be visible only on single map, modelId should be provided + - **start** - start point of the line [line marker only] + - **end** - end point of the line [line marker only] - adds one marker to markers list - returns created `Marker` - examples: @@ -94,6 +98,20 @@ Below is a description of the methods, as well as the types they return. A descr y: 4322, number: 43, }); + window.minerva.data.bioEntities.addSingleMarker({ + type: 'line', + color: '#106AD7', + opacity: 1, + modelId: 52, + start: { + x: 8723, + y: 4322, + }, + end: { + x: 4438, + y: 1124, + }, + }); ``` ##### `removeSingleMarker` diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.test.ts index 0510f57200aec90f58f3e448f674bf965b90ca9a..1176b3ededf0e8dd2f3c158cd9cd0de2dec0409b 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.test.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.test.ts @@ -1,5 +1,5 @@ -import { MarkerSurface } from '@/redux/markers/markers.types'; import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { MarkerSurface } from '@/types/models'; import { parseSurfaceMarkersToBioEntityRender } from './parseSurfaceMarkersToBioEntityRender'; const MARKERS: MarkerSurface[] = [ diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.ts index bd6461e9cf928d45ebee30fc191ca1daa137a523..2f20250e75370f4c7770039a13a745deb7c3aa8c 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/parseSurfaceMarkersToBioEntityRender.ts @@ -1,6 +1,6 @@ import { ZERO } from '@/constants/common'; -import { MarkerSurface } from '@/redux/markers/markers.types'; import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { MarkerSurface } from '@/types/models'; import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString'; export const parseSurfaceMarkersToBioEntityRender = ( diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts index 705debb673e1e8b5d38460e73fc1d8bcbe11fd1b..f024c157616bbcb03f482fc476448c01f51de71e 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkerSingleFeature.ts @@ -1,4 +1,4 @@ -import { Marker } from '@/redux/markers/markers.types'; +import { MarkerWithPosition } from '@/types/models'; import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; @@ -6,7 +6,7 @@ import { getPinFeature } from './getPinFeature'; import { getPinStyle } from './getPinStyle'; export const getMarkerSingleFeature = ( - marker: Marker, + marker: MarkerWithPosition, { pointToProjection, }: { diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkersFeatures.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkersFeatures.ts index fa12fb5e44b4ed9c587b7d8394aeb170fdc03658..84855d478bf15184ca93d5400c3a1734e551650a 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkersFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getMarkersFeatures.ts @@ -1,10 +1,10 @@ -import { Marker } from '@/redux/markers/markers.types'; +import { MarkerWithPosition } from '@/types/models'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; import { getMarkerSingleFeature } from './getMarkerSingleFeature'; export const getMarkersFeatures = ( - markers: Marker[], + markers: MarkerWithPosition[], { pointToProjection, }: { diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts index 51d4f8d363697addadc40b00dae775744530ac0d..8b5bf85bf3928e174a47033808766e91815ff14b 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinFeature.ts @@ -1,15 +1,20 @@ import { ZERO } from '@/constants/common'; import { HALF } from '@/constants/dividers'; import { FEATURE_TYPE } from '@/constants/features'; -import { Marker } from '@/redux/markers/markers.types'; -import { BioEntity } from '@/types/models'; +import { BioEntity, MarkerWithPosition } from '@/types/models'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import isUUID from 'is-uuid'; import { Feature } from 'ol'; import { Point } from 'ol/geom'; export const getPinFeature = ( - { x, y, width, height, id }: Pick<BioEntity, 'id' | 'width' | 'height' | 'x' | 'y'> | Marker, + { + x, + y, + width, + height, + id, + }: Pick<BioEntity, 'id' | 'width' | 'height' | 'x' | 'y'> | MarkerWithPosition, pointToProjection: UsePointToProjectionResult, ): Feature => { const isMarker = isUUID.anyNonNil(`${id}`); diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index 37bb871b4f61222e962bc4295482964514fc689e..10e1d4e4d4b9ea419ce7e8411cb9e7bc4a8c59f3 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -1,7 +1,8 @@ /* eslint-disable no-magic-numbers */ import { LINE_COLOR, LINE_WIDTH } from '@/constants/canvas'; +import { markersLinesCurrentMapDataSelector } from '@/redux/markers/markers.selectors'; import { allReactionsSelectorOfCurrentMap } from '@/redux/reactions/reactions.selector'; -import { Reaction } from '@/types/models'; +import { MarkerLine, Reaction } from '@/types/models'; import { LinePoint } from '@/types/reactions'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; @@ -15,17 +16,27 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getLineFeature } from './getLineFeature'; +const getLinePoints = ({ start, end }: Pick<MarkerLine, 'start' | 'end'>): LinePoint => [ + start, + end, +]; + const getReactionsLines = (reactions: Reaction[]): LinePoint[] => - reactions.map(({ lines }) => lines.map(({ start, end }): LinePoint => [start, end])).flat(); + reactions.map(({ lines }) => lines.map(getLinePoints)).flat(); export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { const pointToProjection = usePointToProjection(); const reactions = useSelector(allReactionsSelectorOfCurrentMap); + const markers = useSelector(markersLinesCurrentMapDataSelector); const reactionsLines = getReactionsLines(reactions); + const markerLines = markers.map(getLinePoints); const reactionsLinesFeatures = useMemo( - () => reactionsLines.map(linePoint => getLineFeature(linePoint, pointToProjection)), - [reactionsLines, pointToProjection], + () => + [...reactionsLines, ...markerLines].map(linePoint => + getLineFeature(linePoint, pointToProjection), + ), + [reactionsLines, markerLines, pointToProjection], ); const vectorSource = useMemo(() => { diff --git a/src/models/markerSchema.ts b/src/models/markerSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ea2951127426e996020182d9ac1ea71c9b498e5 --- /dev/null +++ b/src/models/markerSchema.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { positionSchema } from './positionSchema'; + +export const markerTypeSchema = z.union([ + z.literal('pin'), + z.literal('surface'), + z.literal('line'), +]); + +const markerBaseSchema = z.object({ + type: markerTypeSchema, + id: z.string(), + color: z.string(), + opacity: z.number(), + number: z.number().optional(), + modelId: z.number().optional(), +}); + +const markerWithPositionBaseSchema = markerBaseSchema.extend({ + x: z.number(), + y: z.number(), +}); + +export const markerPinSchema = markerWithPositionBaseSchema.extend({ + width: z.number().optional(), + height: z.number().optional(), +}); + +export const markerSurfaceSchema = markerWithPositionBaseSchema.extend({ + width: z.number(), + height: z.number(), +}); + +export const markerLineSchema = markerBaseSchema.extend({ + start: positionSchema, + end: positionSchema, +}); + +export const markerWithPositionSchema = z.union([markerPinSchema, markerSurfaceSchema]); + +export const markerSchema = z.union([markerPinSchema, markerSurfaceSchema, markerLineSchema]); diff --git a/src/redux/markers/markers.reducers.ts b/src/redux/markers/markers.reducers.ts index d0cad10d7ae84ae2c9659529480243e5f7664ea3..89e912b22e25ee0466053f3aaff6e5201607d6c9 100644 --- a/src/redux/markers/markers.reducers.ts +++ b/src/redux/markers/markers.reducers.ts @@ -1,5 +1,6 @@ +import { Marker } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; -import { Marker, MarkersState } from './markers.types'; +import { MarkersState } from './markers.types'; export const setMarkersDataReducer = ( state: MarkersState, diff --git a/src/redux/markers/markers.selectors.ts b/src/redux/markers/markers.selectors.ts index da6987b3ec82807bd85d2fdaf65dc82cd0015423..18a3a82cd46f6c4d4062aec3f60d3a2000edcb5e 100644 --- a/src/redux/markers/markers.selectors.ts +++ b/src/redux/markers/markers.selectors.ts @@ -1,8 +1,8 @@ +import { Marker, MarkerLine, MarkerSurface, MarkerWithPosition } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; import { currentModelIdSelector } from '../models/models.selectors'; import { rootSelector } from '../root/root.selectors'; -import { MarkerSurface } from './markers.types'; -import { isMarkerSurface } from './markers.utils'; +import { isMarkerLine, isMarkerSurface } from './markers.utils'; export const markersSelector = createSelector(rootSelector, state => state.markers); @@ -20,7 +20,12 @@ export const markersPinsDataSelector = createSelector(markersDataSelector, marke export const markersPinsOfCurrentMapDataSelector = createSelector( markersDataOfCurrentMapSelector, - markersData => markersData.filter(m => m.type === 'pin'), + (markersData): MarkerWithPosition[] => + markersData + .filter(m => m.type === 'pin') + .filter((marker: Marker): marker is MarkerWithPosition => + Boolean('x' in marker && 'y' in marker), + ), ); export const markersSufraceSelector = createSelector(markersDataSelector, markersData => @@ -31,3 +36,8 @@ export const markersSufraceOfCurrentMapDataSelector = createSelector( markersDataOfCurrentMapSelector, (markers): MarkerSurface[] => markers.filter(isMarkerSurface), ); + +export const markersLinesCurrentMapDataSelector = createSelector( + markersDataOfCurrentMapSelector, + (markers): MarkerLine[] => markers.filter(isMarkerLine), +); diff --git a/src/redux/markers/markers.types.ts b/src/redux/markers/markers.types.ts index 4ea8307815f564d7ca8826cc2dcb4efc987d5a40..62f9b9f19857bcd745706146650d2841ffbef8b4 100644 --- a/src/redux/markers/markers.types.ts +++ b/src/redux/markers/markers.types.ts @@ -1,30 +1,9 @@ -export type MarkerType = 'pin' | 'surface'; +import { Marker } from '@/types/models'; -interface MarkerBase { - type: MarkerType; - id: string; - color: string; - opacity: number; - x: number; - y: number; - number?: number; - modelId?: number; +export interface MarkerWithOptionalId extends Omit<Marker, 'id'> { + id?: string; } -export interface MarkerPin extends MarkerBase { - width?: number; - height?: number; -} - -export interface MarkerSurface extends MarkerBase { - width: number; - height: number; -} - -export type Marker = MarkerPin | MarkerSurface; - -export type MarkerWithoutId = Omit<Marker, 'id'>; - export interface MarkersState { data: Marker[]; } diff --git a/src/redux/markers/markers.utils.ts b/src/redux/markers/markers.utils.ts index 08b862e3c28e819a260999cdfbc4214c411762d8..c0cd24ec1065109fb8440e3a48b3f5dac8c41aca 100644 --- a/src/redux/markers/markers.utils.ts +++ b/src/redux/markers/markers.utils.ts @@ -1,4 +1,9 @@ -import { Marker, MarkerSurface } from './markers.types'; +import { Marker, MarkerLine, MarkerSurface } from '@/types/models'; export const isMarkerSurface = (marker: Marker): marker is MarkerSurface => - Boolean(marker?.width && marker?.height && marker.type === 'surface'); + Boolean('width' in marker && marker?.width && marker?.height && marker.type === 'surface'); + +export const isMarkerLine = (marker: Marker): marker is MarkerLine => + Boolean( + 'start' in marker && 'end' in marker && marker?.start && marker?.end && marker.type === 'line', + ); diff --git a/src/redux/models/marker.mock.ts b/src/redux/models/marker.mock.ts index 951b5d627443f509194a2deec7739dd682e8e48d..657f28933e2e5a49fc3758bb070c1e4a3284ec31 100644 --- a/src/redux/models/marker.mock.ts +++ b/src/redux/models/marker.mock.ts @@ -1,4 +1,4 @@ -import { MarkerPin, MarkerSurface } from '../markers/markers.types'; +import { MarkerPin, MarkerSurface } from '@/types/models'; export const SURFACE_MARKER: MarkerSurface = { type: 'surface', diff --git a/src/services/pluginsManager/bioEntities/addSingleMarker.test.ts b/src/services/pluginsManager/bioEntities/addSingleMarker.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..63c3269ac1d8b0db2a96bfd365c275adf79aa4b2 --- /dev/null +++ b/src/services/pluginsManager/bioEntities/addSingleMarker.test.ts @@ -0,0 +1,102 @@ +import { addMarkerToMarkersData } from '@/redux/markers/markers.slice'; +import { MarkerWithOptionalId } from '@/redux/markers/markers.types'; +import { Marker, MarkerLine, MarkerPin, MarkerSurface } from '@/types/models'; +import { ZodError } from 'zod'; +import { store } from '../../../redux/store'; +import { addSingleMarker } from './addSingleMarker'; + +jest.mock('../../../redux/store'); + +const VALID_MARKERS: MarkerWithOptionalId[] = [ + { + id: 'id-123', + type: 'pin', + color: '#F48C41', + opacity: 0.68, + x: 1000, + y: 200, + number: 75, + modelId: 52, + } as MarkerPin, + { + type: 'surface', + color: '#106AD7', + opacity: 0.24, + x: 442, + y: 442, + width: 600, + height: 500, + number: 37, + } as MarkerSurface, + { + type: 'line', + color: '#106AD7', + opacity: 0.7312, + modelId: 52, + start: { + x: 1200, + y: 432, + }, + end: { + x: 332, + y: 112, + }, + } as MarkerLine, +]; + +export const INVALID_MARKERS: unknown[] = [ + { + id: 'id-123', + type: 'pin', + x: 1000, + y: 200, + number: 75, + modelId: 52, + }, + { + type: 'surface', + color: '#106AD7', + opacity: 0.24, + x: 442, + number: 37, + }, + { + type: 'line', + color: '#106AD7', + opacity: 0.7312, + modelId: 52, + start: { + x: 1200, + y: 432, + }, + }, + { + id: 123345, + color: '#106AD7', + opacity: 0.7312, + modelId: 52, + start: { + x: 1200, + y: 432, + }, + }, +]; + +describe('addSingleMarker - plugin method', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + it.each(VALID_MARKERS)( + 'should dispatch addMarkerToMarkersData and return valid marker with id', + marker => { + const markerResults = addSingleMarker(marker); + + expect(dispatchSpy).toHaveBeenCalledWith( + addMarkerToMarkersData({ ...marker, id: markerResults.id } as Marker), + ); + }, + ); + + it.each(INVALID_MARKERS)('should throw error', marker => { + expect(() => addSingleMarker(marker as MarkerWithOptionalId)).toThrow(ZodError); + }); +}); diff --git a/src/services/pluginsManager/bioEntities/addSingleMarker.ts b/src/services/pluginsManager/bioEntities/addSingleMarker.ts index 0b496795439fd0ad034442cae31d0e4493717133..2475a78bbe6d5227e94fdcd886cbfd5aae74fba5 100644 --- a/src/services/pluginsManager/bioEntities/addSingleMarker.ts +++ b/src/services/pluginsManager/bioEntities/addSingleMarker.ts @@ -1,11 +1,15 @@ +import { markerSchema } from '@/models/markerSchema'; import { addMarkerToMarkersData } from '@/redux/markers/markers.slice'; -import { Marker, MarkerWithoutId } from '@/redux/markers/markers.types'; +import { MarkerWithOptionalId } from '@/redux/markers/markers.types'; import { store } from '@/redux/store'; +import { Marker } from '@/types/models'; import { v4 as uuidv4 } from 'uuid'; -export const addSingleMarker = (markerWithoutId: MarkerWithoutId): Marker => { +export const addSingleMarker = (markerWithoutId: MarkerWithOptionalId): Marker => { const { dispatch } = store; - const marker = { id: uuidv4(), ...markerWithoutId }; + const marker = { id: uuidv4(), ...markerWithoutId } as Marker; + markerSchema.parse(marker); + dispatch(addMarkerToMarkersData(marker)); return marker; diff --git a/src/services/pluginsManager/bioEntities/getAllMarkers.ts b/src/services/pluginsManager/bioEntities/getAllMarkers.ts index 1ed70af6a1683d76bafe6f1d5afc2fd27b0e36ee..2fa459d3283d7f9d20d4ba501184097e72f44211 100644 --- a/src/services/pluginsManager/bioEntities/getAllMarkers.ts +++ b/src/services/pluginsManager/bioEntities/getAllMarkers.ts @@ -1,6 +1,6 @@ import { markersDataSelector } from '@/redux/markers/markers.selectors'; -import { Marker } from '@/redux/markers/markers.types'; import { store } from '@/redux/store'; +import { Marker } from '@/types/models'; export const getAllMarkers = (): Marker[] => { const { getState } = store; diff --git a/src/services/pluginsManager/bioEntities/getShownElements.types.ts b/src/services/pluginsManager/bioEntities/getShownElements.types.ts index dcbacd297ec5a113d4ebba2ea3c469a1b2e8ffcb..210a0e0194f54a7f0f1ecb6e49f17f01502c00a2 100644 --- a/src/services/pluginsManager/bioEntities/getShownElements.types.ts +++ b/src/services/pluginsManager/bioEntities/getShownElements.types.ts @@ -1,5 +1,4 @@ -import { Marker } from '@/redux/markers/markers.types'; -import { BioEntity } from '@/types/models'; +import { BioEntity, Marker } from '@/types/models'; export interface GetShownElementsPluginMethodResult { content: BioEntity[]; diff --git a/src/types/models.ts b/src/types/models.ts index 39fada22f2a548136555a69f8e10d10a130f28b4..aa0b1d3f83bb1d7c9acae257d9e39eda1e986127 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -23,6 +23,14 @@ import { mapOverlay, uploadedOverlayFileContentSchema, } from '@/models/mapOverlay'; +import { + markerLineSchema, + markerPinSchema, + markerSchema, + markerSurfaceSchema, + markerTypeSchema, + markerWithPositionSchema, +} from '@/models/markerSchema'; import { mapModelSchema } from '@/models/modelSchema'; import { organism } from '@/models/organism'; import { @@ -102,3 +110,9 @@ export type GeneVariant = z.infer<typeof geneVariant>; export type TargetSearchNameResult = z.infer<typeof targetSearchNameResult>; export type TargetElement = z.infer<typeof targetElementSchema>; export type SubmapConnection = z.infer<typeof submapConnection>; +export type MarkerType = z.infer<typeof markerTypeSchema>; +export type MarkerPin = z.infer<typeof markerPinSchema>; +export type MarkerSurface = z.infer<typeof markerSurfaceSchema>; +export type MarkerLine = z.infer<typeof markerLineSchema>; +export type MarkerWithPosition = z.infer<typeof markerWithPositionSchema>; +export type Marker = z.infer<typeof markerSchema>;