Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve resource location design #646

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 111 additions & 15 deletions deployment/frontend/src/components/datasets/sections/DataFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ import { useLayersFromRW } from '@/utils/queryHooks'
import { useActiveCharts, useActiveLayerGroups } from '@/utils/storeHooks'
import { TabularResource } from '../visualizations/Visualizations'
import { APIButton } from './datafiles/API'
import { Layer, Map, MapRef, Marker, Source } from 'react-map-gl'
import {
Layer,
Map,
MapLayerMouseEvent,
MapRef,
Marker,
Source,
} from 'react-map-gl'
import GeocoderControl from '@/components/search/GeocoderControl'
import { useQuery } from 'react-query'
import { UseFormReturn, useForm } from 'react-hook-form'
Expand Down Expand Up @@ -67,12 +74,16 @@ function LocationSearch({
geojsons,
formObj,
open,
toggleDatafileToDownload,
}: {
geojsons: any[]
open: boolean
formObj: UseFormReturn<LocationSearchFormType>
toggleDatafileToDownload: (datafile: Resource) => void
}) {
const { setValue } = formObj
const [cursor, setCursor] = useState('grab')

const mapRef = useRef<MapRef | null>(null)
const accessToken =
'pk.eyJ1IjoicmVzb3VyY2V3YXRjaCIsImEiOiJjbHNueG5idGIwOXMzMmp0ZzE1NWVjZDV1In0.050LmRm-9m60lrzhpsKqNA'
Expand All @@ -95,6 +106,71 @@ function LocationSearch({
}
)

// Store a reference to the geojsons by layer ID for lookup
const layerGeojsonMap = useRef<Record<string, any>>({})

// Set up the layer reference map whenever geojsons change
useEffect(() => {
const newMap: Record<string, any> = {}
geojsons
.filter((g) => !g.address)
.forEach((geojson, index) => {
const fillLayerId = `fill-layer-${index}`
const lineLayerId = `line-layer-${index}`
newMap[fillLayerId] = geojson
newMap[lineLayerId] = geojson
})
layerGeojsonMap.current = newMap
}, [geojsons])

// Handle map clicks and determine if a layer was clicked
const handleMapClick = useCallback(
(event: MapLayerMouseEvent) => {
if (!mapRef.current) return

// Get the features at the clicked point
const features = mapRef.current.queryRenderedFeatures(event.point)

// Check if any of our layers were clicked
if (features.length > 0) {
const clickedLayerId = features[0]?.layer.id
if (!clickedLayerId) return
const geojson = layerGeojsonMap.current[clickedLayerId]

if (geojson) {
console.log('Layer clicked:', geojson)
// Call your toggle function or other actions
if (geojson.datafile) {
toggleDatafileToDownload(geojson.datafile)
}
}
}
},
[toggleDatafileToDownload]
)

// Handle mouse movement to change cursor
const handleMouseMove = useCallback((event: MapLayerMouseEvent) => {
if (!mapRef.current) return

const features = mapRef.current.queryRenderedFeatures(event.point)

// Check if any of our layers are under the mouse
if (features.length > 0) {
const hoveredLayerId = features[0]?.layer.id
if (!hoveredLayerId) return

// If the layer ID is in our mapping, it's one of our layers
if (layerGeojsonMap.current[hoveredLayerId]) {
setCursor('default') // Set to default arrow cursor
} else {
setCursor('grab') // Set to the default map grabbing cursor
}
} else {
setCursor('grab') // Default map cursor
}
}, [])

const onUpdate = useCallback((e: any) => {
const newFeatures = {}
for (const f of e.features) {
Expand Down Expand Up @@ -127,14 +203,16 @@ function LocationSearch({
dragRotate={false}
touchZoomRotate={false}
mapStyle="mapbox://styles/mapbox/streets-v9"
onClick={handleMapClick}
onMouseMove={handleMouseMove}
cursor={cursor}
>
<GeocoderControl
mapboxAccessToken="pk.eyJ1IjoicmVzb3VyY2V3YXRjaCIsImEiOiJjbHNueG5idGIwOXMzMmp0ZzE1NWVjZDV1In0.050LmRm-9m60lrzhpsKqNA"
position="bottom-right"
placeholder="Search datafiles by location"
initialValue={formObj.getValues('location')}
onResult={(e) => {
console.log('GEOCODING RESULT', e)
setValue('bbox', [
[e.result.bbox[0], e.result.bbox[1]],
[e.result.bbox[2], e.result.bbox[3]],
Expand All @@ -159,11 +237,13 @@ function LocationSearch({
.map((geojson, index) => (
<Source key={index} type="geojson" data={geojson}>
<Layer
id={`fill-layer-${index}`} // Unique ID is crucial
type="fill"
paint={{
'fill-color': geojson.filtered
? '#023020'
: '#BAE1BD',
'fill-color':
geojson.filtered || geojson.selected
? '#023020'
: '#BAE1BD',
'fill-opacity': 0.3,
}}
/>
Expand Down Expand Up @@ -275,16 +355,19 @@ export function DataFiles({
const geojsons = useMemo(() => {
return filteredDatafilesByName
.filter((r) => r.spatial_type !== 'global')
.filter((r) => r.spatial_address || r.spatial_geom)
.map((df) => ({
...df.spatial_geom,
address: df.spatial_address,
selected: datafilesToDownload.some((f) => f.id === df.id),
filtered:
filteredDatafiles.length !==
filteredDatafilesByName.length &&
filteredDatafiles.some((f) => f.id === df.id),
id: df.id,
datafile: df,
}))
}, [filteredDatafilesByName, filteredDatafiles])
}, [filteredDatafilesByName, filteredDatafiles, datafilesToDownload])

const addDatafileToDownload = (datafile: Resource) => {
setDatafilesToDownload((prev) => [...prev, datafile])
Expand All @@ -295,6 +378,14 @@ export function DataFiles({
)
}

const toggleDatafileToDownload = (datafile: Resource) => {
if (datafilesToDownload.some((f) => f.id === datafile.id)) {
removeDatafileToDownload(datafile)
} else {
addDatafileToDownload(datafile)
}
}

const filteredUploadedDatafiles = filteredDatafiles.filter(
(r) => r.url_type === 'upload' || r.url_type === 'link'
)
Expand All @@ -317,7 +408,7 @@ export function DataFiles({
toast('Failed to send your information', {
type: 'error',
})
setOpen(false)
setOpenDownload(false)
},
})

Expand Down Expand Up @@ -345,7 +436,7 @@ export function DataFiles({
toast("You'll receive an email when the file is ready", {
type: 'success',
})
setOpen(false)
setOpenDownload(false)
},
onError: (err) => {
console.error(err)
Expand All @@ -356,7 +447,7 @@ export function DataFiles({
}
)
}
const [open, setOpen] = useState(false)
const [openDownload, setOpenDownload] = useState(false)
return (
<>
<div className="relative py-4">
Expand All @@ -367,13 +458,15 @@ export function DataFiles({
placeholder="Search datafiles by title or description"
/>
<MagnifyingGlassIcon className="w-5 h-5 text-black absolute top-[30px] right-4" />
{dataset.is_approved && (
<Disclosure>
{dataset.is_approved && geojsons.length > 0 && (
<Disclosure defaultOpen={true}>
{({ open }) => (
<>
<Disclosure.Button as={Fragment}>
<Button className="my-2 ml-auto group sm:flex items-center justify-center h-8 rounded-md gap-x-1 bg-blue-100 hover:bg-blue-800 hover:text-white text-blue-800 text-xs px-3">
Filter by Location
{open
? 'Collapse'
: 'Open Filter by Location'}
<GlobeAmericasIcon className="group-hover:text-white h-4 w-4 text-blue-800 mb-1" />
</Button>
</Disclosure.Button>
Expand Down Expand Up @@ -437,6 +530,9 @@ export function DataFiles({
)}
>
<LocationSearch
toggleDatafileToDownload={
toggleDatafileToDownload
}
open={open}
geojsons={geojsons}
formObj={formObj}
Expand Down Expand Up @@ -544,7 +640,7 @@ export function DataFiles({
</div>
{datafilesToDownload.length > 0 && (
<Button
onClick={() => setOpen(true)}
onClick={() => setOpenDownload(true)}
className="group sm:flex items-center justify-center h-8 rounded-md gap-x-1 bg-blue-100 hover:bg-blue-800 hover:text-white text-blue-800 text-xs px-3"
>
Download Selected Datafiles
Expand Down Expand Up @@ -613,8 +709,8 @@ export function DataFiles({
<DownloadPopup
title="The selected datafiles are being prepared for download"
subtitle="Please enter your information so that you receive the download link via email"
isOpen={open}
onClose={() => setOpen(false)}
isOpen={openDownload}
onClose={() => setOpenDownload(false)}
dataset={dataset}
onSubmit={handleFormSubmit}
downloadButton={
Expand Down
17 changes: 16 additions & 1 deletion deployment/frontend/src/components/search/Draw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function DrawControl(props: DrawControlProps) {
defaultMode: 'simple_select',
modes: { ...MapboxDraw.modes, draw_polygon: DrawRectangle },
}
useControl<MapboxDraw>(
const control = useControl<MapboxDraw>(
() => {
const _draw = new MapboxDraw(_props)
setDraw(_draw)
Expand All @@ -72,6 +72,21 @@ export default function DrawControl(props: DrawControlProps) {
}
)

useEffect(() => {
if (typeof window !== 'undefined') {
const button = document.querySelector(
'.mapbox-gl-draw_ctrl-draw-btn'
)
if (button) {
button.title =
'Area Select Tool, use this to create a reactangle that can be used as a bounding box for your search'
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed"><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h1"/><path d="M14 3h1"/><path d="M14 21h1"/><path d="M3 9v1"/><path d="M21 9v1"/><path d="M3 14v1"/><path d="M21 14v1"/></svg>
<span style="color: black !important; width: auto;">Area Select Tool</span>`
}
}
}, [control])

return null
}

Expand Down
10 changes: 10 additions & 0 deletions deployment/frontend/src/styles/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,13 @@ $dark-pink: #c32d7b;
li.osano-cm-drawer-item:nth-child(5) {
display: none;
}

.mapbox-gl-draw_ctrl-draw-btn {
width: auto !important;
padding: 5px 10px 5px 5px !important;
background: none;
color: black !important;
display: flex !important;
align-items: center;
gap: 0.3rem;
}
Loading