Skip to content

Commit

Permalink
feat: implement purpose filter and wire to map
Browse files Browse the repository at this point in the history
  • Loading branch information
stdavis committed Nov 29, 2024
1 parent b48d036 commit 2f7310f
Show file tree
Hide file tree
Showing 10 changed files with 500 additions and 89 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"Geospatial",
"gnis",
"hostingchannels",
"immer",
"lods",
"noopener",
"noreferrer",
Expand All @@ -30,9 +31,11 @@
"tailwindcss",
"tanstack",
"topo",
"UDWR",
"ugrc",
"usgs",
"vite",
"WILDADMIN",
"wkid",
"wrimaps"
],
Expand Down
24 changes: 23 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@
"@ugrc/layer-selector": "^6.2.7",
"@ugrc/utah-design-system": "^1.4.1",
"firebase": "^10.13.0",
"immer": "^10.1.1",
"ky": "^1.7.1",
"react": "^18.3.1",
"react-aria": "^3.34.3",
"react-aria-components": "^1.3.3",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-stately": "^3.32.2"
"react-stately": "^3.32.2",
"use-immer": "^0.10.0"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
Expand Down
61 changes: 32 additions & 29 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import { useOverlayTriggerState } from 'react-stately';
import { MapContainer } from './components';
import { useAnalytics, useFirebaseApp } from './components/contexts';
import { FilterProvider } from './components/contexts/FilterProvider';
import Filter from './components/Filter';
import { useMap } from './components/hooks';
import { DnrLogo } from './components/Logo';
Expand Down Expand Up @@ -116,40 +117,42 @@ export default function App() {
</Header>
{isAuthenticated ? (
<section className="relative flex min-h-0 flex-1 gap-2 overflow-x-hidden md:mr-2">
<Drawer main state={sideBarState} {...sideBarTriggerProps}>
<div className="mx-2 mb-2 grid grid-cols-1 gap-2">
<Filter />
<h2 className="text-xl font-bold">Map controls</h2>
<div className="flex flex-col gap-4 rounded border border-zinc-200 p-3 dark:border-zinc-700">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Sherlock {...masqueradeSherlockOptions} label="Find a stream" />
</ErrorBoundary>
<FilterProvider>
<Drawer main state={sideBarState} {...sideBarTriggerProps}>
<div className="mx-2 mb-2 grid grid-cols-1 gap-2">
<Filter />
<h2 className="text-xl font-bold">Map controls</h2>
<div className="flex flex-col gap-4 rounded border border-zinc-200 p-3 dark:border-zinc-700">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Sherlock {...masqueradeSherlockOptions} label="Find a stream" />
</ErrorBoundary>
</div>
<div className="flex flex-col gap-4 rounded border border-zinc-200 p-3 dark:border-zinc-700">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Sherlock {...masqueradeSherlockOptions} label="Find a lake" />
</ErrorBoundary>
</div>
</div>
<div className="flex flex-col gap-4 rounded border border-zinc-200 p-3 dark:border-zinc-700">
</Drawer>
<div className="relative flex flex-1 flex-col rounded">
<div className="relative flex-1 overflow-hidden dark:rounded">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Sherlock {...masqueradeSherlockOptions} label="Find a lake" />
<MapContainer />
</ErrorBoundary>
<Drawer
type="tray"
className="shadow-inner dark:shadow-white/20"
allowFullScreen
state={trayState}
{...trayTriggerProps}
>
<section className="grid gap-2 px-7 pt-2">
<h2 className="text-center">Selected records</h2>
</section>
</Drawer>
</div>
</div>
</Drawer>
<div className="relative flex flex-1 flex-col rounded">
<div className="relative flex-1 overflow-hidden dark:rounded">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<MapContainer />
</ErrorBoundary>
<Drawer
type="tray"
className="shadow-inner dark:shadow-white/20"
allowFullScreen
state={trayState}
{...trayTriggerProps}
>
<section className="grid gap-2 px-7 pt-2">
<h2 className="text-center">Selected records</h2>
</section>
</Drawer>
</div>
</div>
</FilterProvider>
</section>
) : (
<section className="flex flex-1 items-center justify-center">
Expand Down
81 changes: 26 additions & 55 deletions src/components/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,49 @@
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import { useQuery } from '@tanstack/react-query';
import { Button, Checkbox, CheckboxGroup, TextField } from '@ugrc/utah-design-system';
import ky from 'ky';
import { useEffect } from 'react';
import config from '../config';
import { Button, TextField } from '@ugrc/utah-design-system';
import { useEffect, useRef } from 'react';
import { useFilter } from './contexts/FilterProvider';
import Purpose from './filters/Purpose';
import { useMap } from './hooks';
import { getStationQuery } from './queryHelpers';

const emptyDefinition = '1=0';

type DomainValue = {
name: string;
code: string;
};
type Field = {
name: string;
domain: {
codedValues: DomainValue[];
};
};
type FeatureLayerDefinition = {
fields: Field[];
};

async function getPurposes(): Promise<DomainValue[]> {
// TODO: this should probably come from env var
// if we do end up staying with the public service, then it will need to be published to the test server (wrimaps.at.utah.gov)
const url = 'https://wrimaps.utah.gov/arcgis/rest/services/Electrofishing/Public/MapServer/1?f=json';
const responseJson = (await ky(url).json()) as FeatureLayerDefinition;

const purposeField = responseJson.fields.find(
(field: Field) => field.name === config.fieldNames.events.SURVEY_PURPOSE,
);

if (!purposeField) {
throw new Error(`${config.fieldNames.events.SURVEY_PURPOSE} field not found in ${url}`);
}

return purposeField.domain.codedValues;
}

export default function Filter() {
export default function Filter(): JSX.Element {
const { addLayers, mapView } = useMap();
const purposeQuery = useQuery({ queryKey: ['purposes'], queryFn: getPurposes });
const stationsLayer = useRef<FeatureLayer>();
const { filter } = useFilter();

useEffect(() => {
if (!mapView) {
if (!mapView || !addLayers) {
return;
}

const stations = new FeatureLayer({
stationsLayer.current = new FeatureLayer({
url: 'https://wrimaps.utah.gov/arcgis/rest/services/Electrofishing/Public/MapServer/0',
definitionExpression: emptyDefinition,
});
addLayers([stations]);
addLayers([stationsLayer.current]);
}, [addLayers, mapView]);

useEffect(() => {
if (!stationsLayer.current) {
return;
}

if (Object.keys(filter).length > 0) {
const newQuery = getStationQuery(Object.values(filter));
console.log('new query:', newQuery);
stationsLayer.current.definitionExpression = newQuery;
} else {
stationsLayer.current.definitionExpression = emptyDefinition;
}
}, [filter]);

return (
<>
<h2 className="text-xl font-bold">Map filters</h2>
<div className="flex flex-col gap-4 rounded border border-zinc-200 p-3 dark:border-zinc-700">
<div>
<h3 className="text-lg font-semibold">Purpose</h3>
<CheckboxGroup>
{purposeQuery.data?.map(({ name, code }) => (
<div key={code} className="ml-2 flex gap-1">
<Checkbox type="checkbox" id={code} name={code} value={code} />
<label htmlFor={code}>{name}</label>
</div>
))}
</CheckboxGroup>
</div>
<div className="w-30 flex justify-end">
<Button variant="secondary">clear all</Button>
</div>
<Purpose />
</div>
<div className="flex flex-col gap-4 rounded border border-zinc-200 p-3 dark:border-zinc-700">
<h3 className="text-lg font-semibold">Species and length</h3>
Expand Down
63 changes: 63 additions & 0 deletions src/components/contexts/FilterProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createContext, Dispatch, useContext } from 'react';
import { useImmerReducer } from 'use-immer';

export type QueryInfo = {
where: string;
table: string;
};
type FilterState = Record<string, QueryInfo>;
type FilterKeys = 'purpose';
type Action =
| {
type: 'UPDATE_TABLE';
filterKey: FilterKeys;
value: QueryInfo;
}
| {
type: 'CLEAR_TABLE';
filterKey: FilterKeys;
}
| {
type: 'CLEAR_FILTER';
};

const FilterContext = createContext<{ filter: FilterState; filterDispatch: Dispatch<Action> } | null>(null);

const initialState: FilterState = {};

function reducer(draft: FilterState, action: Action): FilterState {
switch (action.type) {
case 'UPDATE_TABLE':
draft[action.filterKey] = action.value;

console.log('updated filter:', JSON.stringify(draft, null, 2));

return draft;

case 'CLEAR_TABLE':
delete draft[action.filterKey];

console.log('updated filter:', JSON.stringify(draft, null, 2));

return draft;

case 'CLEAR_FILTER':
return initialState;
}
}

export function FilterProvider({ children }: { children: React.ReactNode }) {
const [filter, filterDispatch] = useImmerReducer(reducer, initialState);

return <FilterContext.Provider value={{ filter, filterDispatch }}>{children}</FilterContext.Provider>;
}

export function useFilter() {
const context = useContext(FilterContext);

if (!context) {
throw new Error('useFilter must be used within a FilterProvider');
}

return context;
}
Loading

0 comments on commit 2f7310f

Please sign in to comment.