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

Adding layers to EUDR analysis #1128

Merged
merged 4 commits into from
Mar 7, 2024
Merged
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
5 changes: 5 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
"test": "start-server-and-test 'yarn build && yarn start' http://localhost:3000/auth/signin 'nyc --reporter nyc-report-lcov-absolute yarn cypress:headless'"
},
"dependencies": {
"@deck.gl/carto": "^8.9.35",
"@deck.gl/core": "8.8.6",
"@deck.gl/extensions": "8.8.6",
"@deck.gl/geo-layers": "8.8.6",
"@deck.gl/layers": "8.8.6",
"@deck.gl/mapbox": "8.8.6",
"@deck.gl/mesh-layers": "8.8.6",
"@deck.gl/react": "^8.9.35",
"@dnd-kit/core": "5.0.3",
"@dnd-kit/modifiers": "5.0.0",
"@dnd-kit/sortable": "6.0.1",
Expand All @@ -35,7 +37,9 @@
"@loaders.gl/core": "3.3.1",
"@luma.gl/constants": "8.5.18",
"@radix-ui/react-collapsible": "1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "1.1.3",
"@radix-ui/react-select": "2.0.0",
"@radix-ui/react-slot": "1.0.2",
Expand All @@ -45,6 +49,7 @@
"@tanstack/react-query": "^4.2.1",
"@tanstack/react-table": "8.13.2",
"@tanstack/react-virtual": "3.0.1",
"@turf/bbox": "^6.5.0",
"autoprefixer": "10.2.5",
"axios": "1.3.4",
"chroma-js": "2.1.2",
Expand Down
29 changes: 29 additions & 0 deletions client/src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';

import { cn } from '@/lib/utils';

const Popover = PopoverPrimitive.Root;

const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverContent };
110 changes: 110 additions & 0 deletions client/src/containers/analysis-eudr/map/component.tsx
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;
1 change: 1 addition & 0 deletions client/src/containers/analysis-eudr/map/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component';
74 changes: 74 additions & 0 deletions client/src/containers/analysis-eudr/map/legend/component.tsx
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;
1 change: 1 addition & 0 deletions client/src/containers/analysis-eudr/map/legend/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component';
33 changes: 33 additions & 0 deletions client/src/containers/analysis-eudr/map/legend/item.tsx
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 client/src/containers/analysis-eudr/map/zoom/component.tsx
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;
1 change: 1 addition & 0 deletions client/src/containers/analysis-eudr/map/zoom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component';
25 changes: 25 additions & 0 deletions client/src/hooks/eudr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,28 @@ export const useEUDRSuppliers = <T = Supplier[]>(
},
);
};

export const usePlotGeometries = <T = Supplier[]>(
params?: {
producersIds: string[];
originsId: string[];
materialsId: string[];
geoRegionIds: string[];
},
options: UseQueryOptions<Supplier[], unknown, T> = {},
) => {
return useQuery(
['eudr-geo-features-collection', params],
() =>
apiService
.request<{ geojson }>({
method: 'GET',
url: '/eudr/geo-features/collection',
params,
})
.then((response) => response.data.geojson),
{
...options,
},
);
};
Loading
Loading