From dc96beb62cca5543e9754314e67daaa994ad47d4 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 10 Oct 2024 12:59:52 +0100 Subject: [PATCH] refactor(frontend): shared library map components Restore the shared library map components for both `BaseMap` and `DataMap` components. App-specific state is passed into each as props. --- frontend/src/app/map/BaseMap.tsx | 24 ++------ frontend/src/app/map/DataMap.tsx | 82 +++---------------------- frontend/src/app/map/MapView.tsx | 10 ++-- frontend/src/lib/data-map/BaseMap.tsx | 35 +++++++++++ frontend/src/lib/data-map/DataMap.tsx | 86 +++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 99 deletions(-) create mode 100644 frontend/src/lib/data-map/BaseMap.tsx create mode 100644 frontend/src/lib/data-map/DataMap.tsx diff --git a/frontend/src/app/map/BaseMap.tsx b/frontend/src/app/map/BaseMap.tsx index f64a1c0b..a0ae8bdf 100644 --- a/frontend/src/app/map/BaseMap.tsx +++ b/frontend/src/app/map/BaseMap.tsx @@ -1,9 +1,9 @@ import { MapViewState } from 'deck.gl/typed'; import { FC, ReactNode } from 'react'; -import { Map } from 'react-map-gl/maplibre'; import { useRecoilState, useRecoilValue } from 'recoil'; import { mapViewStateState, useSyncMapUrl } from 'app/state/map-view/map-view-state'; +import { BaseMap } from 'lib/data-map/BaseMap'; import { backgroundState, showLabelsState } from './layers/layers-state'; import { useBasemapStyle } from './use-basemap-style'; @@ -11,11 +11,7 @@ export interface BaseMapProps { children?: ReactNode; } -/** - * Displays a react-map-gl basemap component. - * Accepts children such as a DeckGLOverlay, HUD controls etc - */ -export const BaseMap: FC = ({ children }) => { +export const BaseMapContainer: FC = ({ children }) => { const background = useRecoilValue(backgroundState); const showLabels = useRecoilValue(showLabelsState); const [viewState, setViewState] = useRecoilState(mapViewStateState); @@ -27,20 +23,8 @@ export const BaseMap: FC = ({ children }) => { } return ( - + {children} - + ); }; diff --git a/frontend/src/app/map/DataMap.tsx b/frontend/src/app/map/DataMap.tsx index 21e0b219..0734b1cb 100644 --- a/frontend/src/app/map/DataMap.tsx +++ b/frontend/src/app/map/DataMap.tsx @@ -1,32 +1,16 @@ -import type { MapboxOverlay } from '@deck.gl/mapbox/typed'; -import { useMap } from 'react-map-gl/maplibre'; -import { FC, useCallback, useEffect, useMemo, useRef } from 'react'; +import { FC, useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { interactionGroupsState } from 'app/state/layers/interaction-groups'; import { viewLayersFlatState } from 'app/state/layers/view-layers-flat'; import { useSaveViewLayers, viewLayersParamsState } from 'app/state/layers/view-layers-params'; -import { useInteractions } from 'lib/state/interactions/use-interactions'; -import { useTriggerMemo } from 'lib/hooks/use-trigger-memo'; -import { useDataLoadTrigger } from 'lib/data-map/use-data-load-trigger'; - -import { DeckGLOverlay } from 'lib/map/DeckGLOverlay'; -import { ViewLayer, ViewLayerParams } from 'lib/data-map/view-layers'; -import { LayersList } from 'deck.gl/typed'; +import { DataMap } from 'lib/data-map/DataMap'; import { backgroundState, showLabelsState } from './layers/layers-state'; import { useBasemapStyle } from './use-basemap-style'; -// set a convention where the view layer id is either the first part of the deck id before the @ sign, or it's the whole id -function lookupViewForDeck(deckLayerId: string) { - return deckLayerId.split('@')[0]; -} - -export const DataMap: FC = () => { - const deckRef = useRef(); - const { current: map } = useMap(); - const zoom = map.getMap().getZoom(); +export const DataMapContainer: FC = () => { const background = useRecoilValue(backgroundState); const showLabels = useRecoilValue(showLabelsState); const viewLayers = useRecoilValue(viewLayersFlatState); @@ -41,62 +25,12 @@ export const DataMap: FC = () => { const interactionGroups = useRecoilValue(interactionGroupsState); - const dataLoaders = useMemo( - () => - viewLayers - .map((vl) => vl.dataAccessFn?.(vl.styleParams?.colorMap?.fieldSpec)?.dataLoader) - .filter(Boolean), - [viewLayers], - ); - - const dataLoadTrigger = useDataLoadTrigger(dataLoaders); - - const layersFunction = useCallback( - ({ zoom }: { zoom: number }) => - viewLayers.map((viewLayer) => - makeDeckLayers(viewLayer, viewLayersParams[viewLayer.id], zoom, firstLabelId), - ) as LayersList, - [firstLabelId, viewLayers, viewLayersParams], - ); - - const { onHover, onClick, layerFilter, pickingRadius } = useInteractions( - viewLayers, - lookupViewForDeck, - interactionGroups, - ); - - const layers = useTriggerMemo( - () => layersFunction({ zoom }), - [layersFunction, zoom], - dataLoadTrigger, - ); - return ( - 'default'} - layers={layers} - layerFilter={layerFilter} - onHover={(info) => deckRef.current && onHover?.(info, deckRef.current)} - onClick={(info) => deckRef.current && onClick?.(info, deckRef.current)} - pickingRadius={pickingRadius} + ); }; - -function makeDeckLayers( - viewLayer: ViewLayer, - viewLayerParams: ViewLayerParams, - zoom: number, - beforeId: string | undefined, -) { - return viewLayer.fn({ - deckProps: { id: viewLayer.id, pickable: !!viewLayer.interactionGroup, beforeId }, - zoom, - ...viewLayerParams, - }); -} diff --git a/frontend/src/app/map/MapView.tsx b/frontend/src/app/map/MapView.tsx index bd70ba4e..921a001e 100644 --- a/frontend/src/app/map/MapView.tsx +++ b/frontend/src/app/map/MapView.tsx @@ -1,8 +1,8 @@ import { Suspense, useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; -import { BaseMap } from './BaseMap'; -import { DataMap } from './DataMap'; +import { BaseMapContainer } from './BaseMap'; +import { DataMapContainer } from './DataMap'; import { DataMapTooltip } from 'lib/data-map/DataMapTooltip'; import { MapBoundsFitter, mapFitBoundsState } from 'lib/map/MapBoundsFitter'; import { MapHud } from 'lib/map/hud/MapHud'; @@ -99,14 +99,14 @@ const MapViewContent = () => { const isMobile = useIsMobile(); return ( - - + + {isMobile ? : } - + ); }; diff --git a/frontend/src/lib/data-map/BaseMap.tsx b/frontend/src/lib/data-map/BaseMap.tsx new file mode 100644 index 00000000..87f7d392 --- /dev/null +++ b/frontend/src/lib/data-map/BaseMap.tsx @@ -0,0 +1,35 @@ +import { MapViewState } from 'deck.gl/typed'; +import { StyleSpecification } from 'maplibre-gl'; +import { FC, ReactNode } from 'react'; +import { Map } from 'react-map-gl/maplibre'; + +export interface BaseMapProps { + children?: ReactNode; + mapStyle: StyleSpecification; + onMove: ({ viewState }: { viewState: MapViewState }) => void; + viewState: MapViewState; +} + +/** + * Displays a react-map-gl basemap component. + * Accepts children such as a DeckGLOverlay, HUD controls etc + */ +export const BaseMap: FC = ({ children, mapStyle, onMove, viewState }) => { + return ( + + {children} + + ); +}; diff --git a/frontend/src/lib/data-map/DataMap.tsx b/frontend/src/lib/data-map/DataMap.tsx new file mode 100644 index 00000000..de40a119 --- /dev/null +++ b/frontend/src/lib/data-map/DataMap.tsx @@ -0,0 +1,86 @@ +import type { MapboxOverlay } from '@deck.gl/mapbox/typed'; +import { useMap } from 'react-map-gl/maplibre'; +import { FC, useCallback, useMemo, useRef } from 'react'; + +import { useInteractions } from 'lib/state/interactions/use-interactions'; +import { useTriggerMemo } from 'lib/hooks/use-trigger-memo'; +import { useDataLoadTrigger } from 'lib/data-map/use-data-load-trigger'; +import { InteractionGroupConfig } from 'lib/data-map/types'; +import { DeckGLOverlay } from 'lib/map/DeckGLOverlay'; +import { ViewLayer, ViewLayerParams } from 'lib/data-map/view-layers'; +import { LayersList } from 'deck.gl/typed'; + +// set a convention where the view layer id is either the first part of the deck id before the @ sign, or it's the whole id +function lookupViewForDeck(deckLayerId: string) { + return deckLayerId.split('@')[0]; +} + +export const DataMap: FC<{ + firstLabelId: string; + interactionGroups: Map; + viewLayers: ViewLayer[]; + viewLayersParams: Record; +}> = ({ firstLabelId, interactionGroups, viewLayers, viewLayersParams }) => { + const deckRef = useRef(); + const { current: map } = useMap(); + const zoom = map.getMap().getZoom(); + + const dataLoaders = useMemo( + () => + viewLayers + .map((vl) => vl.dataAccessFn?.(vl.styleParams?.colorMap?.fieldSpec)?.dataLoader) + .filter(Boolean), + [viewLayers], + ); + + const dataLoadTrigger = useDataLoadTrigger(dataLoaders); + + const layersFunction = useCallback( + ({ zoom }: { zoom: number }) => + viewLayers.map((viewLayer) => + makeDeckLayers(viewLayer, viewLayersParams[viewLayer.id], zoom, firstLabelId), + ) as LayersList, + [firstLabelId, viewLayers, viewLayersParams], + ); + + const { onHover, onClick, layerFilter, pickingRadius } = useInteractions( + viewLayers, + lookupViewForDeck, + interactionGroups, + ); + + const layers = useTriggerMemo( + () => layersFunction({ zoom }), + [layersFunction, zoom], + dataLoadTrigger, + ); + + return ( + 'default'} + layers={layers} + layerFilter={layerFilter} + onHover={(info) => deckRef.current && onHover?.(info, deckRef.current)} + onClick={(info) => deckRef.current && onClick?.(info, deckRef.current)} + pickingRadius={pickingRadius} + /> + ); +}; + +function makeDeckLayers( + viewLayer: ViewLayer, + viewLayerParams: ViewLayerParams, + zoom: number, + beforeId: string | undefined, +) { + return viewLayer.fn({ + deckProps: { id: viewLayer.id, pickable: !!viewLayer.interactionGroup, beforeId }, + zoom, + ...viewLayerParams, + }); +}