diff --git a/src/components/Filter.tsx b/src/components/Filter.tsx index f96ce2e..5fb7f09 100644 --- a/src/components/Filter.tsx +++ b/src/components/Filter.tsx @@ -1,5 +1,4 @@ import Extent from '@arcgis/core/geometry/Extent'; -import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; import FeatureEffect from '@arcgis/core/layers/support/FeatureEffect'; import FeatureFilter from '@arcgis/core/layers/support/FeatureFilter'; import { useEffect, useRef } from 'react'; @@ -17,40 +16,18 @@ import { getStationQuery } from './queryHelpers'; const emptyDefinition = '1=0'; export default function Filter(): JSX.Element { - const { addLayers, mapView } = useMap(); - const stationsLayer = useRef(); + const { stationsLayer, mapView } = useMap(); const { filter } = useFilter(); const initialized = useRef(false); const { selectedStationIds, setSelectedStationIds } = useSelection(); useEffect(() => { - if (!mapView || !addLayers || initialized.current) { + if (!mapView || !stationsLayer) { return; } - stationsLayer.current = new FeatureLayer({ - url: config.urls.stations, - definitionExpression: emptyDefinition, - outFields: [config.fieldNames.STATION_ID], - renderer: { - // @ts-expect-error - accessor has issues with TS - type: 'simple', - symbol: { - type: 'simple-marker', - color: '#DD5623', - outline: { - color: [0, 0, 0], - width: 1, - }, - size: 12, - }, - }, - }); - - addLayers([stationsLayer.current]); - mapView.on('pointer-move', (event) => { - mapView.hitTest(event, { include: stationsLayer.current as FeatureLayer }).then((response) => { + mapView.hitTest(event, { include: stationsLayer }).then((response) => { if (response.results.length > 0) { mapView.container.style.cursor = 'pointer'; } else { @@ -58,10 +35,11 @@ export default function Filter(): JSX.Element { } }); }); + mapView.on('click', (event) => { mapView .hitTest(event, { - include: stationsLayer.current as FeatureLayer, + include: stationsLayer, }) .then((response) => { if (response.results.length > 0) { @@ -89,26 +67,26 @@ export default function Filter(): JSX.Element { }); initialized.current = true; - }, [addLayers, mapView, setSelectedStationIds]); + }, [mapView, setSelectedStationIds, stationsLayer]); useEffect(() => { - if (!stationsLayer.current) { + if (!stationsLayer) { return; } if (Object.keys(filter).length > 0) { const newQuery = getStationQuery(Object.values(filter)); console.log('new station query:', newQuery); - stationsLayer.current.definitionExpression = newQuery; + stationsLayer.definitionExpression = newQuery; } else { - stationsLayer.current.definitionExpression = emptyDefinition; + stationsLayer.definitionExpression = emptyDefinition; } setSelectedStationIds(new Set()); - stationsLayer.current + stationsLayer .queryExtent({ - where: stationsLayer.current.definitionExpression, + where: stationsLayer.definitionExpression, }) .then((result) => { if (mapView) { @@ -129,26 +107,26 @@ export default function Filter(): JSX.Element { } } }); - }, [filter, mapView, setSelectedStationIds]); + }, [filter, mapView, setSelectedStationIds, stationsLayer]); useEffect(() => { - if (!stationsLayer.current) { + if (!stationsLayer) { return; } if (selectedStationIds.size === 0) { // @ts-expect-error null is a valid value - stationsLayer.current.featureEffect = null; + stationsLayer.featureEffect = null; } else { const where = `${config.fieldNames.STATION_ID} IN ('${Array.from(selectedStationIds).join("','")}')`; - stationsLayer.current.featureEffect = new FeatureEffect({ + stationsLayer.featureEffect = new FeatureEffect({ filter: new FeatureFilter({ where, }), excludedEffect: 'opacity(50%)', }); } - }, [selectedStationIds]); + }, [selectedStationIds, stationsLayer]); return ( <> diff --git a/src/components/ResultsGrid.tsx b/src/components/ResultsGrid.tsx index 6eac1ef..1987353 100644 --- a/src/components/ResultsGrid.tsx +++ b/src/components/ResultsGrid.tsx @@ -3,10 +3,12 @@ import { RowSelectionState, Updater } from '@tanstack/react-table'; import { Button, Spinner, Tab, TabList, TabPanel, Tabs, useFirebaseAuth } from '@ugrc/utah-design-system'; import { User } from 'firebase/auth'; import ky from 'ky'; +import { SearchIcon, SquareXIcon } from 'lucide-react'; import config from '../config'; import { useFilter } from './contexts/FilterProvider'; import { useSelection } from './contexts/SelectionProvider'; import Download from './Download'; +import { useMap } from './hooks'; import { getGridQuery, removeIrrelevantWhiteSpace } from './queryHelpers'; import { Table } from './Table'; import { getEventIdsForDownload, getResultOidsFromStationIds, getStationIdsFromResultRows } from './utils'; @@ -184,6 +186,7 @@ const columns = [ export default function ResultsGrid() { const { filter } = useFilter(); + const { stationsLayer, mapView } = useMap(); const { currentUser } = useFirebaseAuth(); const gridQuery = getGridQuery(Object.values(filter)); @@ -219,23 +222,45 @@ export default function ResultsGrid() { setSelectedStationIds(getStationIdsFromResultRows(data, new Set(Object.keys(newSelection)))); } + const onZoomToSelection = () => { + if (!mapView || !stationsLayer) { + return; + } + + stationsLayer + .queryExtent({ + where: `${config.fieldNames.STATION_ID} IN ('${Array.from(selectedStationIds).join("','")}')`, + }) + .then((result) => { + // handle if only a single feature was selected + if (result.count === 1) { + mapView.goTo({ target: result.extent.center, zoom: 12 }); + } else { + mapView.goTo(result.extent); + } + }); + }; + return ( <> - - Records: {data?.length} + + Results: {data?.length} {selectedStationIds.size > 0 && ( - + <> {' '} | Selected: {Object.keys(rowSelection).length} + - + )} diff --git a/src/components/contexts/MapProvider.tsx b/src/components/contexts/MapProvider.tsx index 7148111..1925528 100644 --- a/src/components/contexts/MapProvider.tsx +++ b/src/components/contexts/MapProvider.tsx @@ -1,8 +1,10 @@ import Graphic from '@arcgis/core/Graphic'; +import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; import MapView from '@arcgis/core/views/MapView'; import { useGraphicManager } from '@ugrc/utilities/hooks'; import PropTypes from 'prop-types'; -import { createContext, ReactNode, useState } from 'react'; +import { createContext, ReactNode, useRef, useState } from 'react'; +import config from '../../config'; export const MapContext = createContext<{ mapView: MapView | null; @@ -10,6 +12,7 @@ export const MapContext = createContext<{ placeGraphic: (graphic: Graphic | Graphic[] | null) => void; zoom: (geometry: __esri.GoToTarget2D) => void; addLayers: (layers: __esri.Layer[]) => void; + stationsLayer: __esri.FeatureLayer | undefined; } | null>(null); export const MapProvider = ({ children }: { children: ReactNode }) => { @@ -46,6 +49,30 @@ export const MapProvider = ({ children }: { children: ReactNode }) => { mapView.map.addMany(layers); }; + const stationsLayer = useRef(); + if (mapView && !stationsLayer.current) { + stationsLayer.current = new FeatureLayer({ + url: config.urls.stations, + definitionExpression: '1=0', + outFields: [config.fieldNames.STATION_ID], + renderer: { + // @ts-expect-error - accessor has issues with TS + type: 'simple', + symbol: { + type: 'simple-marker', + color: '#DD5623', + outline: { + color: [0, 0, 0], + width: 1, + }, + size: 12, + }, + }, + }); + + addLayers([stationsLayer.current]); + } + return ( { placeGraphic, zoom, addLayers, + stationsLayer: stationsLayer.current, }} > {children}