Skip to content

Commit

Permalink
refactor(frontend): generalise TooltipContent (#44)
Browse files Browse the repository at this point in the history
* refactor(frontend): generalise TooltipContent

Generalise TooltipContent with a `useTooltipLayers` hook, which supplies a map of layer IDs to their respective tooltip components and layer state. This should make it easier to add new layers, by extending the map.

* refactor(frontend): interaction groups as a dynamic map

Add/remove new interactive layers by adding entries to/removing entries from the Map in `src/config/interaction-groups`.
  • Loading branch information
eatyourgreens authored Jun 26, 2024
1 parent 98cf81d commit 27ec7ba
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 167 deletions.
12 changes: 8 additions & 4 deletions frontend/src/config/assets/asset-view-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { selectableMvtLayer } from 'lib/deck/layers/selectable-mvt-layer';
import { getAssetDataFormats } from './data-formats';
import { ASSETS_SOURCE } from './source';
import { VectorTarget } from 'lib/data-map/interactions/use-interactions';

interface ViewLayerMetadata {
group: string;
Expand All @@ -31,11 +32,13 @@ export function assetViewLayer(
params: {
assetId,
},
fn: ({ deckProps, zoom, styleParams, selection }: ViewLayerFunctionOptions) =>
selectableMvtLayer(
fn: ({ deckProps, zoom, styleParams, selection }: ViewLayerFunctionOptions) => {
const target = selection.target as VectorTarget;
const selectedFeatureId = target?.feature.id;
return selectableMvtLayer(
{
selectionOptions: {
selectedFeatureId: selection?.target.feature.id,
selectedFeatureId,
polygonOffset: selectionPolygonOffset,
},
dataLoaderOptions: {
Expand All @@ -47,7 +50,8 @@ export function assetViewLayer(
data: ASSETS_SOURCE.getDataUrl({ assetId }),
},
...customFn({ zoom, styleParams }),
),
);
},
dataAccessFn: customDataAccessFn,
dataFormatsFn: getAssetDataFormats,
};
Expand Down
82 changes: 48 additions & 34 deletions frontend/src/config/interaction-groups.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
import { InteractionGroupConfig } from 'lib/data-map/interactions/use-interactions';
import { makeConfig } from 'lib/helpers';

export const INTERACTION_GROUPS = makeConfig<InteractionGroupConfig, string>([
{
id: 'assets',
type: 'vector',
pickingRadius: 8,
pickMultiple: false,
usesAutoHighlight: true,
},
{
id: 'hazards',
type: 'raster',
pickMultiple: true,
},
{
id: 'regions',
type: 'vector',
pickingRadius: 8,
pickMultiple: false,
},
{
id: 'solutions',
type: 'vector',
pickingRadius: 8,
usesAutoHighlight: true,
pickMultiple: false,
},
{
id: 'drought',
type: 'vector',
pickingRadius: 8,
usesAutoHighlight: true,
pickMultiple: false,
},
export const INTERACTION_GROUPS = new Map<string, InteractionGroupConfig>([
[
'assets',
{
id: 'assets',
type: 'vector',
pickingRadius: 8,
pickMultiple: false,
usesAutoHighlight: true,
},
],
[
'hazards',
{
id: 'hazards',
type: 'raster',
pickMultiple: true,
},
],
[
'regions',
{
id: 'regions',
type: 'vector',
pickingRadius: 8,
pickMultiple: false,
},
],
[
'solutions',
{
id: 'solutions',
type: 'vector',
pickingRadius: 8,
usesAutoHighlight: true,
pickMultiple: false,
},
],
[
'drought',
{
id: 'drought',
type: 'vector',
pickingRadius: 8,
usesAutoHighlight: true,
pickMultiple: false,
},
],
]);
12 changes: 8 additions & 4 deletions frontend/src/config/regions/population-view-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { featureProperty } from 'lib/deck/props/data-source';
import { border, fillColor } from 'lib/deck/props/style';
import { RegionLevel } from './metadata';
import { REGIONS_SOURCE } from './source';
import { VectorTarget } from 'lib/data-map/interactions/use-interactions';

export function populationViewLayer(regionLevel: RegionLevel): ViewLayer {
const source = REGIONS_SOURCE;
Expand Down Expand Up @@ -35,9 +36,11 @@ export function populationViewLayer(regionLevel: RegionLevel): ViewLayer {
getDataLabel: () => 'Population density',
getValueFormatted: (value: number) => `${value.toLocaleString()}/km²`,
}),
fn: ({ deckProps, zoom, selection }) =>
selectableMvtLayer(
{ selectionOptions: { selectedFeatureId: selection?.target.feature.id } },
fn: ({ deckProps, zoom, selection }) => {
const target = selection.target as VectorTarget;
const selectedFeatureId = target?.feature.id;
return selectableMvtLayer(
{ selectionOptions: { selectedFeatureId } },
deckProps,
{
data: source.getDataUrl({ regionLevel }),
Expand All @@ -47,6 +50,7 @@ export function populationViewLayer(regionLevel: RegionLevel): ViewLayer {
{
highlightColor: [0, 255, 255, 100],
},
),
);
},
};
}
3 changes: 2 additions & 1 deletion frontend/src/details/features/FeatureSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ErrorBoundary } from 'lib/react/ErrorBoundary';
import { Box } from '@mui/system';
import { DeselectButton } from 'details/DeselectButton';
import { MobileTabContentWatcher } from 'pages/map/layouts/mobile/tab-has-content';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';

export const FeatureSidebar: FC = () => {
const featureSelection = useRecoilValue(selectionState('assets'));
Expand All @@ -17,7 +18,7 @@ export const FeatureSidebar: FC = () => {
const {
target: { feature },
viewLayer,
} = featureSelection;
} = featureSelection as InteractionTarget<VectorTarget>;

return (
<SidePanel position="relative">
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/details/solutions/SolutionsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { SolutionsSidebarContent } from './SolutionsSidebarContent';
import { MobileTabContentWatcher } from 'pages/map/layouts/mobile/tab-has-content';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';

export const SolutionsSidebar: FC = () => {
const featureSelection = useRecoilValue(selectionState('solutions'));
Expand All @@ -16,7 +17,7 @@ export const SolutionsSidebar: FC = () => {
const {
target: { feature },
viewLayer,
} = featureSelection;
} = featureSelection as InteractionTarget<VectorTarget>;

return (
<SidePanel position="relative">
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/lib/data-map/interactions/interaction-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { atom, atomFamily, selector } from 'recoil';

import { isReset } from 'lib/recoil/is-reset';

import { InteractionTarget } from './use-interactions';
import { InteractionTarget, RasterTarget, VectorTarget } from './use-interactions';

type IT = InteractionTarget<any> | InteractionTarget<any>[];
type InteractionLayer = InteractionTarget<VectorTarget> | InteractionTarget<RasterTarget>;
type IT = InteractionLayer | InteractionLayer[];

export function hasHover(target: IT) {
if (Array.isArray(target)) {
Expand All @@ -24,7 +25,7 @@ export const hoverPositionState = atom({
default: null,
});

export const selectionState = atomFamily<InteractionTarget<any>, string>({
export const selectionState = atomFamily<InteractionLayer, string>({
key: 'selectionState',
default: null,
});
Expand Down
81 changes: 31 additions & 50 deletions frontend/src/lib/data-map/interactions/use-interactions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import DeckGL, { Deck, PickInfo } from 'deck.gl';
import { readPixelsToArray } from '@luma.gl/core';
import keyBy from 'lodash/keyBy';
import filter from 'lodash/filter';
import groupBy from 'lodash/groupBy';
import mapValues from 'lodash/mapValues';
import { useCallback, useEffect, useMemo } from 'react';
Expand Down Expand Up @@ -80,28 +78,28 @@ function processPickedObject(
info: PickInfo<any>,
type: InteractionStyle,
groupName: string,
viewLayerLookup: Record<string, ViewLayer>,
viewLayerLookup: (id: string) => ViewLayer,
lookupViewForDeck: (deckLayerId: string) => string,
) {
const deckLayerId = info.layer.id;
const viewLayerId = lookupViewForDeck(deckLayerId);
const target = processTargetByType(type, info);

return (
target && {
interactionGroup: groupName,
interactionStyle: type,
viewLayer: viewLayerLookup[viewLayerId],
target,
}
);
return (target && {
interactionGroup: groupName,
interactionStyle: type,
viewLayer: viewLayerLookup(viewLayerId),
target,
}) as InteractionLayer;
}

type InteractionLayer = InteractionTarget<VectorTarget> | InteractionTarget<RasterTarget>;

function useSetInteractionGroupState(
stateFamily: RecoilStateFamily<InteractionTarget<any> | InteractionTarget<any>[], string>,
stateFamily: RecoilStateFamily<InteractionLayer | InteractionLayer[], string>,
) {
return useRecoilCallback(({ set }) => {
return (groupName: string, value: InteractionTarget<any> | InteractionTarget<any>[]) => {
return (groupName: string, value: InteractionLayer | InteractionLayer[]) => {
set(stateFamily(groupName), value);
};
});
Expand All @@ -110,26 +108,18 @@ function useSetInteractionGroupState(
export function useInteractions(
viewLayers: ViewLayer[],
lookupViewForDeck: (deckLayerId: string) => string,
interactionGroups: InteractionGroupConfig[],
interactionGroups: Map<string, InteractionGroupConfig>,
) {
const setHoverXY = useSetRecoilState(hoverPositionState);

const setInteractionGroupHover = useSetInteractionGroupState(hoverState);
const setInteractionGroupSelection = useSetInteractionGroupState(selectionState);

const interactionGroupLookup = useMemo(() => keyBy(interactionGroups, 'id'), [interactionGroups]);
const [primaryGroup] = [...interactionGroups.keys()];
const primaryGroupPickingRadius = interactionGroups.get(primaryGroup).pickingRadius;

const primaryGroup = interactionGroups[0].id;
const primaryGroupPickingRadius = interactionGroupLookup[primaryGroup].pickingRadius;
const interactiveLayers = viewLayers.filter((x) => x.interactionGroup);

const interactiveLayers = useMemo(
() => viewLayers.filter((x) => x.interactionGroup),
[viewLayers],
);
const viewLayerLookup = useMemo(
() => keyBy(interactiveLayers, (layer) => layer.id),
[interactiveLayers],
);
const activeGroups = useMemo(
() => groupBy(interactiveLayers, (viewLayer) => viewLayer.interactionGroup),
[interactiveLayers],
Expand All @@ -146,17 +136,18 @@ export function useInteractions(
const onHover = useCallback(
(info: any, deck: Deck) => {
const { x, y } = info;
const viewLayerLookup = (id: string) => viewLayers.find((x) => x.id === id);

for (const [groupName, layers] of Object.entries(activeGroups)) {
const layerIds = layers.map((layer) => layer.id);
const interactionGroup = interactionGroupLookup[groupName];
const interactionGroup = interactionGroups.get(groupName);
const { type, pickingRadius: radius, pickMultiple } = interactionGroup;

const pickingParams = { x, y, layerIds, radius };

if (pickMultiple) {
const pickedObjects: PickInfo<any>[] = deck.pickMultipleObjects(pickingParams);
const interactionTargets: InteractionTarget<any>[] = pickedObjects
const interactionTargets: InteractionLayer[] = pickedObjects
.map((info) =>
processPickedObject(info, type, groupName, viewLayerLookup, lookupViewForDeck),
)
Expand All @@ -165,7 +156,7 @@ export function useInteractions(
setInteractionGroupHover(groupName, interactionTargets);
} else {
const info: PickInfo<any> = deck.pickObject(pickingParams);
const interactionTarget: InteractionTarget<any> =
const interactionTarget: InteractionLayer =
info && processPickedObject(info, type, groupName, viewLayerLookup, lookupViewForDeck);

setInteractionGroupHover(groupName, interactionTarget);
Expand All @@ -176,21 +167,23 @@ export function useInteractions(
},
[
activeGroups,
interactionGroups,
lookupViewForDeck,
interactionGroupLookup,
setHoverXY,
setInteractionGroupHover,
viewLayerLookup,
viewLayers,
],
);

const onClick = useCallback(
(info: any, deck: DeckGL) => {
const { x, y } = info;
const viewLayerLookup = (id: string) => viewLayers.find((x) => x.id === id);

for (const [groupName, viewLayers] of Object.entries(activeGroups)) {
const viewLayerIds = viewLayers.map((layer) => layer.id);

const interactionGroup = interactionGroupLookup[groupName];
const interactionGroup = interactionGroups.get(groupName);
const { type, pickingRadius: radius } = interactionGroup;

// currently only supports selecting vector features
Expand All @@ -203,35 +196,23 @@ export function useInteractions(
}
}
},
[
activeGroups,
lookupViewForDeck,
interactionGroupLookup,
setInteractionGroupSelection,
viewLayerLookup,
],
[activeGroups, interactionGroups, lookupViewForDeck, setInteractionGroupSelection, viewLayers],
);

/**
* Interaction groups which should be rendered during the hover picking pass
*/
const hoverPassGroups = useMemo(
() =>
new Set(
filter(
interactionGroups,
(group) => group.id === primaryGroup || group.usesAutoHighlight,
).map((group) => group.id),
),
[interactionGroups, primaryGroup],
);
const hoverPassGroups = [...interactionGroups.values()]
.filter((group) => group.usesAutoHighlight || group.id === primaryGroup)
.map((group) => group.id);

const layerFilter = ({ layer: deckLayer, renderPass }) => {
if (renderPass === 'picking:hover') {
const viewLayerId = lookupViewForDeck(deckLayer.id);
const interactionGroup = viewLayerId && viewLayerLookup[viewLayerId]?.interactionGroup;
const viewLayerLookup = (id: string) => viewLayers.find((x) => x.id === id);
const interactionGroup = viewLayerLookup(viewLayerId)?.interactionGroup;

return interactionGroup ? hoverPassGroups.has(interactionGroup) : false;
return interactionGroup ? hoverPassGroups.includes(interactionGroup) : false;
}
return true;
};
Expand Down
Loading

0 comments on commit 27ec7ba

Please sign in to comment.