Skip to content

Commit

Permalink
feat: initial filter by project type implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
stdavis committed Jun 17, 2022
1 parent 853bf8b commit f5007cd
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 60 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
"singleQuote": true,
"printWidth": 120
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!@arcgis)/"
]
},
"devDependencies": {
"bestzip": "^2.2.1",
"eslint-config-prettier": "^8.5.0",
Expand Down
94 changes: 94 additions & 0 deletions src/components/AdvancedControls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import React from 'react';
import { Button, Col, Collapse, Container, Row } from 'reactstrap';
import Checkbox from './Checkbox';

export default function AdvancedControls({ projectTypes, labelColors, dispatch, selectedProjectTypes, disabled }) {
const [isOpen, setIsOpen] = React.useState(true);
const toggle = () => setIsOpen((current) => !current);

return (
<>
<div className="d-flex justify-content-center">
<Button onClick={toggle} size="sm" outline title="Advanced Filter" className="py-0">
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronUp} />
</Button>
</div>
<Collapse isOpen={isOpen}>
<Container fluid className="p-0">
<div>Filter By Project Type:</div>
<Row>
<Col>
{Object.keys(projectTypes.road).map((name) => (
<Checkbox
key={name}
uniqueId={`${name}-road`}
label={name}
checked={selectedProjectTypes.road.includes(name)}
color={labelColors?.road}
onChange={() => dispatch({ type: 'projectType', payload: name, meta: 'road' })}
disabled={disabled}
/>
))}
</Col>
<Col>
{Object.keys(projectTypes.transit).map((name) => (
<Checkbox
key={name}
uniqueId={`${name}-transit`}
label={name}
checked={selectedProjectTypes.transit.includes(name)}
color={labelColors?.transit}
onChange={() => dispatch({ type: 'projectType', payload: name, meta: 'transit' })}
disabled={disabled}
/>
))}
</Col>
</Row>
<Row className="mt-2">
<Col>
{Object.keys(projectTypes.activeTransportation)
.slice(0, Object.keys(projectTypes.activeTransportation).length / 2)
.map((name) => (
<Checkbox
key={name}
uniqueId={`${name}-activeTransportation`}
label={name}
checked={selectedProjectTypes.activeTransportation.includes(name)}
color={labelColors?.activeTransportation}
onChange={() => dispatch({ type: 'projectType', payload: name, meta: 'activeTransportation' })}
disabled={disabled}
/>
))}
</Col>
<Col>
{Object.keys(projectTypes.activeTransportation)
.slice(Object.keys(projectTypes.activeTransportation).length / 2)
.map((name) => (
<Checkbox
key={name}
uniqueId={`${name}-activeTransportation`}
label={name}
checked={selectedProjectTypes.activeTransportation.includes(name)}
color={labelColors?.activeTransportation}
onChange={() => dispatch({ type: 'projectType', payload: name, meta: 'activeTransportation' })}
disabled={disabled}
/>
))}
</Col>
</Row>
</Container>
</Collapse>
</>
);
}

AdvancedControls.propTypes = {
projectTypes: PropTypes.object.isRequired,
labelColors: PropTypes.object,
selectedProjectTypes: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
};
22 changes: 22 additions & 0 deletions src/components/Checkbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import PropTypes from 'prop-types';
import { FormGroup, Input, Label } from 'reactstrap';

export default function Checkbox({ label, checked, color, onChange, uniqueId, disabled }) {
return (
<FormGroup check inline>
<Input id={uniqueId ?? label} type="checkbox" checked={checked} onChange={onChange} disabled={disabled} />{' '}
<Label check for={uniqueId ?? label} style={{ marginBottom: 0, color }}>
{` ${label}`}
</Label>
</FormGroup>
);
}

Checkbox.propTypes = {
label: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
color: PropTypes.string,
onChange: PropTypes.func.isRequired,
uniqueId: PropTypes.string,
disabled: PropTypes.bool.isRequired,
};
116 changes: 98 additions & 18 deletions src/components/Filter.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import FeatureFilter from '@arcgis/core/layers/support/FeatureFilter';
import { faList } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import clsx from 'clsx';
Expand All @@ -7,22 +8,68 @@ import { ErrorBoundary } from 'react-error-boundary';
import { Alert, Button, Card, CardBody, CardHeader, Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap';
import { useImmerReducer } from 'use-immer';
import config from '../services/config';
import AdvancedControls from './AdvancedControls';
import './Filter.scss';
import SimpleControls from './SimpleControls';
import { useMapLayers } from './utils';
import { addOrRemove, getLabelColor, useMapLayers } from './utils';

export function getQuery(draft, geometryType) {
let simpleQuery;
if (draft.display === PHASE) {
// phase is a numeric field
simpleQuery = `${config.fieldNames[draft.display]} IN (${draft[draft.display].join(',')})`;
} else {
// mode is a text field
simpleQuery = `${config.fieldNames[draft.display]} IN ('${draft[draft.display].join("','")}')`;
}

const { road, transit, activeTransportation } = draft.projectTypes[draft.display];
const roadInfos = road.map((name) => config.projectTypes.road[name]);
const transitInfos = transit.map((name) => config.projectTypes.transit[name]);
const activeTransportationInfos = activeTransportation.map((name) => config.projectTypes.activeTransportation[name]);
const selectedProjectTypeInfos = [...roadInfos, ...transitInfos, ...activeTransportationInfos];

const projectTypeQueries = [];
for (const info of selectedProjectTypeInfos) {
if (info[geometryType]) {
projectTypeQueries.push(info[geometryType]);
}
}

if (projectTypeQueries.length > 0) {
return `${simpleQuery} AND ((${projectTypeQueries.join(') OR (')}))`;
}

return simpleQuery;
}

function reducer(draft, action) {
const updateLayerDefinitions = () => {
draft.layerDefinitions[`${draft.display}Points`] = getQuery(draft, 'points');
draft.layerDefinitions[`${draft.display}Lines`] = getQuery(draft, 'lines');
};

switch (action.type) {
case 'display':
draft.display = action.payload;

break;

case 'simple':
if (draft[action.meta].includes(action.payload)) {
draft[action.meta] = draft[action.meta].filter((mode) => mode !== action.payload);
} else {
draft[action.meta].push(action.payload);
}
draft[draft.display] = addOrRemove(draft[draft.display], action.payload);

updateLayerDefinitions();

break;

case 'projectType':
draft.projectTypes[draft.display][action.meta] = addOrRemove(
draft.projectTypes[draft.display][action.meta],
action.payload
);

updateLayerDefinitions();

break;

default:
Expand All @@ -36,6 +83,24 @@ const initialState = {
display: MODE,
mode: Object.values(config.symbolValues.mode),
phase: Object.values(config.symbolValues.phase),
projectTypes: {
[MODE]: {
road: Object.keys(config.projectTypes.road),
transit: Object.keys(config.projectTypes.transit),
activeTransportation: Object.keys(config.projectTypes.activeTransportation),
},
[PHASE]: {
road: Object.keys(config.projectTypes.road),
transit: Object.keys(config.projectTypes.transit),
activeTransportation: Object.keys(config.projectTypes.activeTransportation),
},
},
layerDefinitions: {
modePoints: null,
modeLines: null,
phasePoints: null,
phaseLines: null,
},
};

function ErrorFallback({ error }) {
Expand Down Expand Up @@ -70,21 +135,16 @@ export default function Filter({ mapView }) {
}
}, [layers, state.display]);

// update definition queries
React.useEffect(() => {
if (layers) {
const query = `${config.fieldNames.phase} IN ('${state.phase.join("','")}')`;
layers.phasePoints.definitionExpression = query;
layers.phaseLines.definitionExpression = query;
}
}, [layers, state.phase]);
React.useEffect(() => {
if (layers) {
const query = `${config.fieldNames.mode} IN ('${state.mode.join("','")}')`;
layers.modePoints.definitionExpression = query;
layers.modeLines.definitionExpression = query;
for (const layerKey of Object.keys(layers)) {
layers[layerKey].filter = new FeatureFilter({
where: state.layerDefinitions[layerKey],
});
console.log(`${layers[layerKey].layer.title} filter updated to: \n${state.layerDefinitions[layerKey]}`);
}
}
}, [layers, state.mode]);
}, [layers, state.layerDefinitions]);

return (
<>
Expand Down Expand Up @@ -150,6 +210,18 @@ export default function Filter({ mapView }) {
label: 'Active Transportation',
},
]}
disabled={!layers}
/>
<AdvancedControls
projectTypes={config.projectTypes}
selectedProjectTypes={state.projectTypes.mode}
dispatch={dispatch}
labelColors={{
road: getLabelColor('road', layers?.modePoints),
transit: getLabelColor('transit', layers?.modePoints),
activeTransportation: getLabelColor('activeTransportation', layers?.modePoints),
}}
disabled={!layers}
/>
</TabPane>
<TabPane tabId={PHASE}>
Expand Down Expand Up @@ -183,6 +255,14 @@ export default function Filter({ mapView }) {
label: 'Unfunded',
},
]}
disabled={!layers}
/>
<AdvancedControls
projectTypes={config.projectTypes}
selectedProjectTypes={state.projectTypes.phase}
state={state}
dispatch={dispatch}
disabled={!layers}
/>
</TabPane>
</TabContent>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Filter.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.filter-card {
width: 450px;
width: 475px;
}
44 changes: 13 additions & 31 deletions src/components/SimpleControls.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
import PropTypes from 'prop-types';
import { Col, Container, FormGroup, Input, Label, Row } from 'reactstrap';
import { Col, Container, Row } from 'reactstrap';
import Checkbox from './Checkbox';
import Swatch from './Swatch';
import { getSymbol } from './utils';

const firstColWidth = 7;

export default function SimpleControls({ type, state, dispatch, groups }) {
export default function SimpleControls({ type, state, dispatch, groups, disabled }) {
const toggle = (value) => {
dispatch({ type: 'simple', meta: type, payload: value });
};

const getSymbol = (layer, value) => {
if (!layer) {
return null;
}

for (const info of layer.renderer.uniqueValueInfos) {
if (info.value === value) {
return info.symbol;
}
}

throw new Error(
`Could not find symbol in layer: "${layer.title}" for value: "${value}" in field: "${layer.renderer.field}"!`
);
dispatch({ type: 'simple', payload: value });
};

return (
Expand All @@ -39,18 +25,13 @@ export default function SimpleControls({ type, state, dispatch, groups }) {
return (
<Row key={group.value}>
<Col xs={firstColWidth}>
<FormGroup check inline>
<Input
id={group.value}
type="checkbox"
checked={state[type].includes(group.value)}
onChange={() => toggle(group.value)}
style={{ color: labelColor }}
/>{' '}
<Label check for={group.value} style={{ color: labelColor, marginBottom: 0 }}>
{` ${group.label}`}
</Label>
</FormGroup>
<Checkbox
label={group.label}
checked={state[type].includes(group.value)}
onChange={() => toggle(group.value)}
color={labelColor}
disabled={disabled}
/>
</Col>
<Col>
<Swatch symbol={getSymbol(group.linear, group.value)} />
Expand All @@ -76,4 +57,5 @@ SimpleControls.propTypes = {
})
),
dispatch: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
};
Loading

0 comments on commit f5007cd

Please sign in to comment.