Skip to content

Commit

Permalink
add filter config to protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
buckhalt committed Dec 6, 2024
1 parent 5d1b9f6 commit d6c8fb8
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 55 deletions.
139 changes: 87 additions & 52 deletions lib/interviewer/containers/Interfaces/Geospatial.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,131 @@
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useEffect, useRef, useState } from 'react';
import { type StageProps } from '../Stage';
import type { Protocol } from '~/lib/protocol-validation/schemas/src/8.zod';

const INITIAL_ZOOM = 10; // should this be configurable?

export default function GeospatialInterface({ stage }: { stage: StageProps }) {
type GeospatialStage = Extract<
Protocol['stages'][number],
{ type: 'Geospatial' }
>;

export default function GeospatialInterface({
stage,
}: {
stage: GeospatialStage;
}) {
const mapRef = useRef();
const mapContainerRef = useRef();
const [selectedCensusTract, setSelectedCensusTract] = useState(null);
const { center, data, token } = stage;
const [selection, setSelection] = useState(null);
const { center, token, layers, prompts } = stage;

const filterLayer = layers.find((layer) => layer.filter);

const currentPrompt = prompts[0]; // only one prompt for now

const handleReset = () => {
mapRef.current.flyTo({
zoom: INITIAL_ZOOM,
center,
});
setSelectedCensusTract(null);
mapRef.current.setFilter('selectedCensusTract', ['==', 'namelsad10', '']);

setSelection(null);

if (filterLayer) {
mapRef.current.setFilter(filterLayer.id, ['==', filterLayer.filter, '']);
}
};

useEffect(() => {
mapboxgl.accessToken = token;
const dataSources = [...new Set(layers.map((layer) => layer.data))];

mapRef.current = new mapboxgl.Map({
container: mapContainerRef.current,
center,
zoom: INITIAL_ZOOM,
style: 'mapbox://styles/mapbox/light-v11',
style: 'mapbox://styles/mapbox/light-v11', // should this be configurable?
});

mapRef.current.on('load', () => {
mapRef.current.addSource('geojson-data', {
type: 'geojson',
data,
});
if (!layers) return;

// census tract outlines
mapRef.current.addLayer({
id: 'censusTractsOutlineLayer',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': 'purple',
'line-width': 2,
},
});
mapRef.current.addLayer({
id: 'censusTractsFillLayer',
type: 'fill',
source: 'geojson-data',
paint: {
'fill-color': 'purple',
'fill-opacity': 0.1,
},
dataSources.forEach((dataSource) => {
mapRef.current.addSource('geojson-data', {
// hardcoded source name for now
type: 'geojson',
data: dataSource,
});
});

mapRef.current.addLayer({
id: 'selectedCensusTract',
type: 'fill',
source: 'geojson-data',
paint: {
'fill-color': 'green',
'fill-opacity': 0.5,
},
filter: ['==', 'namelsad10', ''],
// add layers based on the protocol
layers.forEach((layer) => {
if (layer.type === 'line') {
mapRef.current?.addLayer({
id: layer.id,
type: 'line',
source: 'geojson-data',
paint: {
'line-color': layer.color,
'line-width': 2,
},
});
} else if (layer.type === 'fill' && !layer.filter) {
mapRef.current?.addLayer({
id: layer.id,
type: 'fill',
source: 'geojson-data',
paint: {
'fill-color': layer.color,
'fill-opacity': layer.opacity,
},
});
} else if (layer.type === 'fill' && layer.filter) {
mapRef.current?.addLayer({
id: layer.id,
type: 'fill',
source: 'geojson-data',
paint: {
'fill-color': layer.color,
'fill-opacity': layer.opacity,
},
filter: ['==', layer.filter, ''],
});
}
});

// handle click of census tracts
mapRef.current.on('click', 'censusTractsFillLayer', (e) => {
const feature = e.features[0];
const tractId = feature.properties.namelsad10; // census tract name prop. comes from the geojson. this will need to be configured based on the geojson
setSelectedCensusTract(tractId);

mapRef.current.setFilter('selectedCensusTract', [
'==',
'namelsad10',
tractId,
]);
});
// if there's a prompt, configure the click event
if (currentPrompt) {
mapRef.current.on('click', currentPrompt.layer, (e) => {
const feature = e.features[0];
const propToSelect = currentPrompt.mapVariable; // Variable from geojson data
const selected = feature.properties[propToSelect];
setSelection(selected);

// Apply the filter to the selection layer if it exists
filterLayer &&
mapRef.current.setFilter(filterLayer.id, [
'==',
filterLayer.filter,
selected,
]);
});
}
});

return () => {
mapRef.current.remove();
};
}, [center, data, token]);
}, [center, currentPrompt, filterLayer, filterLayer?.filter, layers, token]);

return (
<div className="interface">
<h1>Geospatial Interface</h1>
<div>
<h2>{currentPrompt?.text}</h2>
<button onClick={handleReset}>Reset</button>
<p>Selected: {selectedCensusTract}</p>
<p>Selected: {selection}</p>
</div>
<div className="h-full w-full p-2" ref={mapContainerRef} />
</div>
Expand Down
21 changes: 20 additions & 1 deletion lib/protocol-validation/schemas/src/8.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,26 @@ const geospatialStage = baseStageSchema.extend({
type: z.literal('Geospatial'),
center: z.tuple([z.number(), z.number()]),
token: z.string(),
data: z.string(),
layers: z.array(
z
.object({
id: z.string(),
data: z.string(),
type: z.enum(['line', 'fill']),
color: z.string(),
opacity: z.number().optional(),
filter: z.string().optional(),
})
.strict(),
),
prompts: z
.array(
promptSchema.extend({
layer: z.string(),
mapVariable: z.string(),
}),
)
.min(1),
});

// Combine all stage types
Expand Down
65 changes: 63 additions & 2 deletions lib/test-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,77 @@ export const protocol: Protocol = {
label: 'Chicago Geospatial Interface',
type: 'Geospatial',
center: [-87.6298, 41.8781],
data: '/interviewer/ChicagoCensusTracts.geojson',
token: `${env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN}`,
layers: [
{
id: 'censusTractsOutlineLayer',
data: '/interviewer/ChicagoCensusTracts.geojson',
type: 'line',
color: 'purple',
},
{
id: 'censusTractsFillLayer',
data: '/interviewer/ChicagoCensusTracts.geojson',
type: 'fill',
color: 'purple',
opacity: 0.1,
},
{
id: 'selectedCensusTract',
data: '/interviewer/ChicagoCensusTracts.geojson',
type: 'fill',
color: 'green',
opacity: 0.5,
filter: 'namelsad10',
},
],
prompts: [
{
id: 'censusTractPrompt',
layer: 'censusTractsFillLayer',
mapVariable: 'namelsad10', // variable from geojson data
text: 'Please select a census tract in Chicago',
// TODO: connect to an alter variable
},
],
},
{
id: 'geospatial-interface-2',
label: 'New York Geospatial Interface',
type: 'Geospatial',
center: [-74.006, 40.7128],
data: '/interviewer/NewYorkCensusTracts.geojson',
token: `${env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN}`,
layers: [
{
id: 'censusTractsOutlineLayer',
data: '/interviewer/NewYorkCensusTracts.geojson',
type: 'line',
color: 'blue',
},
{
id: 'censusTractsFillLayer',
data: '/interviewer/NewYorkCensusTracts.geojson',
type: 'fill',
color: 'blue',
opacity: 0.1,
},
{
id: 'selectedCensusTract',
data: '/interviewer/NewYorkCensusTracts.geojson',
type: 'fill',
color: 'orange',
filter: 'NTAName',
opacity: 0.5,
},
],
prompts: [
{
id: 'censusTractPrompt',
layer: 'censusTractsFillLayer',
mapVariable: 'NTAName', // variable from geojson data
text: 'Please select a census tract in New York',
},
],
},
{
id: 'anonymisation-interface',
Expand Down

0 comments on commit d6c8fb8

Please sign in to comment.