From e9a9c8b6ec5e33e35b8be02c60de35e17025dfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Wed, 3 Apr 2024 17:02:42 +0200 Subject: [PATCH] feat: add line marker w/ tests --- docs/plugins/data/bioentities.md | 30 ++++-- ...rseSurfaceMarkersToBioEntityRender.test.ts | 2 +- .../parseSurfaceMarkersToBioEntityRender.ts | 2 +- .../pinsLayer/getMarkerSingleFeature.ts | 4 +- .../config/pinsLayer/getMarkersFeatures.ts | 4 +- .../utils/config/pinsLayer/getPinFeature.ts | 11 +- .../reactionsLayer/useOlMapReactionsLayer.ts | 19 +++- src/models/markerSchema.ts | 41 +++++++ src/redux/markers/markers.reducers.ts | 3 +- src/redux/markers/markers.selectors.ts | 16 ++- src/redux/markers/markers.types.ts | 27 +---- src/redux/markers/markers.utils.ts | 9 +- src/redux/models/marker.mock.ts | 2 +- .../bioEntities/addSingleMarker.test.ts | 102 ++++++++++++++++++ .../bioEntities/addSingleMarker.ts | 10 +- .../bioEntities/getAllMarkers.ts | 2 +- .../bioEntities/getShownElements.types.ts | 3 +- src/types/models.ts | 14 +++ 18 files changed, 245 insertions(+), 56 deletions(-) create mode 100644 src/models/markerSchema.ts create mode 100644 src/services/pluginsManager/bioEntities/addSingleMarker.test.ts diff --git a/docs/plugins/data/bioentities.md b/docs/plugins/data/bioentities.md index e17f46d1..e6d8fd8c 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 0510f572..1176b3ed 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 bd6461e9..2f20250e 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 705debb6..f024c157 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 fa12fb5e..84855d47 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 51d4f8d3..8b5bf85b 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 37bb871b..10e1d4e4 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 00000000..9ea29511 --- /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 d0cad10d..89e912b2 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 da6987b3..18a3a82c 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 4ea83078..62f9b9f1 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 08b862e3..c0cd24ec 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 951b5d62..657f2893 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 00000000..63c3269a --- /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 0b496795..2475a78b 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 1ed70af6..2fa459d3 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 dcbacd29..210a0e01 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 39fada22..aa0b1d3f 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>; -- GitLab