-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
783 additions
and
0 deletions.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
client/src/containers/analysis-eudr-detail/filters/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import YearsRange from './years-range'; | ||
|
||
const EUDRDetailFilters = () => { | ||
return ( | ||
<div className="flex space-x-2"> | ||
<YearsRange /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default EUDRDetailFilters; |
77 changes: 77 additions & 0 deletions
77
client/src/containers/analysis-eudr-detail/filters/years-range/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import React, { useCallback, useMemo } from 'react'; | ||
import { UTCDate } from '@date-fns/utc'; | ||
import { ChevronDown } from 'lucide-react'; | ||
import { format } from 'date-fns'; | ||
|
||
import { useAppDispatch, useAppSelector } from 'store/hooks'; | ||
import { eudr, setFilters } from 'store/features/eudr'; | ||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; | ||
import { Button } from '@/components/ui/button'; | ||
import { Calendar } from '@/components/ui/calendar'; | ||
|
||
import type { DateRange } from 'react-day-picker'; | ||
const dateFormatter = (date: Date) => format(date, 'yyyy-MM-dd'); | ||
|
||
// ! the date range is hardcoded for now | ||
export const DATES_RANGE = ['2020-12-31', dateFormatter(new Date())]; | ||
|
||
const DatesRange = (): JSX.Element => { | ||
const dispatch = useAppDispatch(); | ||
const { | ||
filters: { dates }, | ||
} = useAppSelector(eudr); | ||
|
||
const handleDatesChange = useCallback( | ||
(dates: DateRange) => { | ||
if (dates) { | ||
dispatch( | ||
setFilters({ | ||
dates: { | ||
from: dateFormatter(dates.from), | ||
to: dateFormatter(dates.to), | ||
}, | ||
}), | ||
); | ||
} | ||
}, | ||
[dispatch], | ||
); | ||
|
||
const datesToDate = useMemo(() => { | ||
return { | ||
from: dates.from ? new UTCDate(dates.from) : undefined, | ||
to: dates.to ? new UTCDate(dates.to) : undefined, | ||
}; | ||
}, [dates]); | ||
|
||
return ( | ||
<Popover> | ||
<PopoverTrigger asChild> | ||
<Button | ||
variant="ghost" | ||
className="h-auto space-x-1 border border-gray-200 bg-white shadow-sm" | ||
> | ||
<span className="text-gray-500"> | ||
Deforestation alerts from <span className="text-gray-900">{dates.from || '-'}</span> to{' '} | ||
<span className="text-gray-900">{dates.to || '-'}</span> | ||
</span> | ||
<ChevronDown className="h-4 w-4" /> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent className="flex w-auto space-x-2" align="start"> | ||
<Calendar | ||
mode="range" | ||
numberOfMonths={2} | ||
disabled={{ | ||
before: new UTCDate(DATES_RANGE[0]), | ||
after: new UTCDate(DATES_RANGE[1]), | ||
}} | ||
selected={datesToDate} | ||
onSelect={handleDatesChange} | ||
/> | ||
</PopoverContent> | ||
</Popover> | ||
); | ||
}; | ||
|
||
export default DatesRange; |
110 changes: 110 additions & 0 deletions
110
client/src/containers/analysis-eudr-detail/map/component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { useEffect, useState, useCallback } from 'react'; | ||
import DeckGL from '@deck.gl/react/typed'; | ||
import { GeoJsonLayer } from '@deck.gl/layers/typed'; | ||
import Map from 'react-map-gl/maplibre'; | ||
import { WebMercatorViewport, type MapViewState } from '@deck.gl/core/typed'; | ||
import bbox from '@turf/bbox'; | ||
|
||
import ZoomControl from './zoom'; | ||
import LegendControl from './legend'; | ||
|
||
import BasemapControl from '@/components/map/controls/basemap'; | ||
import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; | ||
import { usePlotGeometries } from '@/hooks/eudr'; | ||
|
||
import type { BasemapValue } from '@/components/map/controls/basemap/types'; | ||
import type { MapStyle } from '@/components/map/types'; | ||
|
||
const EUDRMap = () => { | ||
const [mapStyle, setMapStyle] = useState<MapStyle>('terrain'); | ||
const [viewState, setViewState] = useState<MapViewState>(INITIAL_VIEW_STATE); | ||
|
||
const plotGeometries = usePlotGeometries(); | ||
|
||
const layer: GeoJsonLayer = new GeoJsonLayer({ | ||
id: 'geojson-layer', | ||
data: plotGeometries.data, | ||
// Styles | ||
filled: true, | ||
getFillColor: [255, 176, 0, 84], | ||
stroked: true, | ||
getLineColor: [255, 176, 0, 255], | ||
getLineWidth: 1, | ||
lineWidthUnits: 'pixels', | ||
// Interactive props | ||
pickable: true, | ||
autoHighlight: true, | ||
highlightColor: [255, 176, 0, 255], | ||
}); | ||
|
||
const layers = [layer]; | ||
|
||
const handleMapStyleChange = useCallback((newStyle: BasemapValue) => { | ||
setMapStyle(newStyle); | ||
}, []); | ||
|
||
const handleZoomIn = useCallback(() => { | ||
const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom + 1; | ||
setViewState({ ...viewState, zoom }); | ||
}, [viewState]); | ||
|
||
const handleZoomOut = useCallback(() => { | ||
const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom - 1; | ||
setViewState({ ...viewState, zoom }); | ||
}, [viewState]); | ||
|
||
const fitToPlotBounds = useCallback(() => { | ||
if (!plotGeometries.data) return; | ||
const [minLng, minLat, maxLng, maxLat] = bbox(plotGeometries.data); | ||
const newViewport = new WebMercatorViewport(viewState); | ||
const { longitude, latitude, zoom } = newViewport.fitBounds( | ||
[ | ||
[minLng, minLat], | ||
[maxLng, maxLat], | ||
], | ||
{ | ||
padding: 10, | ||
}, | ||
); | ||
if ( | ||
viewState.latitude !== latitude || | ||
viewState.longitude !== longitude || | ||
viewState.zoom !== zoom | ||
) { | ||
setViewState({ ...viewState, longitude, latitude, zoom }); | ||
} | ||
}, [plotGeometries.data, viewState]); | ||
|
||
// Fit to bounds when data is loaded or changed | ||
useEffect(() => { | ||
if (plotGeometries.data) { | ||
fitToPlotBounds(); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [plotGeometries.data]); | ||
|
||
const handleResize = useCallback(() => { | ||
setTimeout(() => fitToPlotBounds(), 0); | ||
}, [fitToPlotBounds]); | ||
|
||
return ( | ||
<> | ||
<DeckGL | ||
viewState={{ ...viewState }} | ||
onViewStateChange={({ viewState }) => setViewState(viewState as MapViewState)} | ||
controller={{ dragRotate: false }} | ||
layers={layers} | ||
onResize={handleResize} | ||
> | ||
<Map reuseMaps mapStyle={MAP_STYLES[mapStyle]} styleDiffing={false} /> | ||
</DeckGL> | ||
<div className="absolute bottom-10 right-6 z-10 w-10 space-y-2"> | ||
<BasemapControl value={mapStyle} onChange={handleMapStyleChange} /> | ||
<ZoomControl viewState={viewState} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} /> | ||
<LegendControl /> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
export default EUDRMap; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './component'; |
74 changes: 74 additions & 0 deletions
74
client/src/containers/analysis-eudr-detail/map/legend/component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { useState } from 'react'; | ||
import classNames from 'classnames'; | ||
import { MinusIcon, PlusIcon } from '@heroicons/react/outline'; | ||
|
||
import LegendItem from './item'; | ||
|
||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; | ||
import SandwichIcon from '@/components/icons/sandwich'; | ||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; | ||
|
||
const EURDLegend = () => { | ||
const [isOpen, setIsOpen] = useState(false); | ||
const [isExpanded, setIsExpanded] = useState(false); | ||
|
||
return ( | ||
<div> | ||
<Popover open={isOpen} onOpenChange={setIsOpen}> | ||
<PopoverTrigger asChild> | ||
<button | ||
type="button" | ||
className={classNames( | ||
'relative flex h-10 w-10 items-center justify-center rounded-lg p-1.5 text-black transition-colors hover:text-navy-400', | ||
isOpen ? 'bg-navy-400 text-white hover:text-white' : 'bg-white', | ||
)} | ||
> | ||
<SandwichIcon /> | ||
</button> | ||
</PopoverTrigger> | ||
<PopoverContent align="end" side="left" className="p-0"> | ||
<div className="divide-y"> | ||
<h2 className="px-4 py-2 text-sm font-normal">Legend</h2> | ||
<div> | ||
<LegendItem | ||
title="Suppliers with specified splot of land" | ||
description="Suppliers with necessary geographical data of land to assess deforestation risk linked to a commodity." | ||
iconClassName="border-2 border-orange-500 bg-orange-500/30" | ||
/> | ||
</div> | ||
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}> | ||
<CollapsibleTrigger asChild> | ||
<div className="flex items-center justify-end px-4 py-2"> | ||
<button type="button" className="flex items-center space-x-2"> | ||
<div className="text-xs text-navy-400">Add contextual layers</div> | ||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-navy-400"> | ||
{isExpanded ? ( | ||
<MinusIcon className="h-4 w-4 text-white" /> | ||
) : ( | ||
<PlusIcon className="h-4 w-4 text-white" /> | ||
)} | ||
</div> | ||
</button> | ||
</div> | ||
</CollapsibleTrigger> | ||
<CollapsibleContent className="bg-navy-50"> | ||
<LegendItem | ||
title="Forest Cover 2020 (EC JRC)" | ||
description="Spatial explicit description of forest presence and absence in the year 2020." | ||
iconClassName="bg-[#72A950]" | ||
/> | ||
<LegendItem | ||
title="Real time deforestation alerts since 2020 (UMD/GLAD)" | ||
description="Monitor forest disturbance in near-real-time using integrated alerts from three alerting systems." | ||
iconClassName="bg-[#C92A6D]" | ||
/> | ||
</CollapsibleContent> | ||
</Collapsible> | ||
</div> | ||
</PopoverContent> | ||
</Popover> | ||
</div> | ||
); | ||
}; | ||
|
||
export default EURDLegend; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './component'; |
33 changes: 33 additions & 0 deletions
33
client/src/containers/analysis-eudr-detail/map/legend/item.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import classNames from 'classnames'; | ||
|
||
import type { FC, PropsWithChildren } from 'react'; | ||
|
||
type LegendItemProps = { title: string; description: string; iconClassName?: string }; | ||
|
||
const LegendItem: FC<PropsWithChildren<LegendItemProps>> = ({ | ||
title, | ||
description, | ||
children, | ||
iconClassName, | ||
}) => { | ||
return ( | ||
<div className="flex space-x-2 p-4"> | ||
<div | ||
className={classNames( | ||
'mt-0.5 h-3 w-3 shrink-0', | ||
iconClassName ?? 'border-2 border-gray-500 bg-gray-500/30', | ||
)} | ||
/> | ||
<div className="flex-1 space-y-1"> | ||
<div> | ||
<h3 className="text-xs font-normal">{title}</h3> | ||
<div></div> | ||
</div> | ||
<p className="text-xs text-gray-500">{description}</p> | ||
{children} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default LegendItem; |
51 changes: 51 additions & 0 deletions
51
client/src/containers/analysis-eudr-detail/map/zoom/component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { MinusIcon, PlusIcon } from '@heroicons/react/solid'; | ||
import cx from 'classnames'; | ||
|
||
import type { MapViewState } from '@deck.gl/core/typed'; | ||
import type { FC } from 'react'; | ||
|
||
const COMMON_CLASSES = | ||
'p-2 transition-colors bg-white cursor-pointer hover:bg-gray-100 active:bg-navy-50 disabled:bg-gray-100 disabled:opacity-75 disabled:cursor-default'; | ||
|
||
const ZoomControl: FC<{ | ||
viewState: MapViewState; | ||
className?: string; | ||
onZoomIn: () => void; | ||
onZoomOut: () => void; | ||
}> = ({ viewState, className = null, onZoomIn, onZoomOut }) => { | ||
const { zoom, minZoom, maxZoom } = viewState; | ||
|
||
return ( | ||
<div | ||
className={cx( | ||
'ml-auto flex select-none flex-col justify-center gap-[1.5px] overflow-hidden rounded-lg border border-gray-200 text-4xl text-gray-900', | ||
className, | ||
)} | ||
> | ||
<button | ||
className={COMMON_CLASSES} | ||
aria-label="Zoom in" | ||
type="button" | ||
// ? Sometimes, depending on the viewport, the map will not reach zoom 22 but 21.X. | ||
// ? As we have no control over this, we are assuming, if no maxZoom is set, going further zoom level 21 will be considered as the limit | ||
disabled={zoom >= maxZoom || (!maxZoom && zoom > 21)} | ||
onClick={onZoomIn} | ||
> | ||
<PlusIcon className="h-5 w-5" /> | ||
</button> | ||
<button | ||
className={COMMON_CLASSES} | ||
aria-label="Zoom out" | ||
type="button" | ||
// ? Sometimes, depending on the viewport, the map will not reach zoom 0 but 0.X. | ||
// ? As we have no control over this, we are assuming, if no minZoom is set, going below zoom level 1 will be considered as the limit | ||
disabled={zoom <= minZoom || (!minZoom && zoom < 1)} | ||
onClick={onZoomOut} | ||
> | ||
<MinusIcon className="h-5 w-5" /> | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ZoomControl; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './component'; |
Oops, something went wrong.