From 307fbb594d89e047950abca27ea36cd6a1c4bbc6 Mon Sep 17 00:00:00 2001 From: David Inga Date: Wed, 20 Mar 2024 20:05:41 +0100 Subject: [PATCH] fixed compare --- .../containers/analysis-eudr/map/compare.tsx | 411 ++++++++++++++++++ .../analysis-eudr/map/component.tsx | 98 +---- client/src/pages/eudr/index.tsx | 9 +- .../src/pages/eudr/suppliers/[supplierId].tsx | 8 +- 4 files changed, 447 insertions(+), 79 deletions(-) create mode 100644 client/src/containers/analysis-eudr/map/compare.tsx diff --git a/client/src/containers/analysis-eudr/map/compare.tsx b/client/src/containers/analysis-eudr/map/compare.tsx new file mode 100644 index 000000000..d3ff59eef --- /dev/null +++ b/client/src/containers/analysis-eudr/map/compare.tsx @@ -0,0 +1,411 @@ +import { useState, useCallback, useEffect, useMemo } from 'react'; +import DeckGL from '@deck.gl/react/typed'; +import { BitmapLayer, GeoJsonLayer } from '@deck.gl/layers/typed'; +import Map, { useMap, useControl } from 'react-map-gl/maplibre'; +import { WebMercatorViewport } from '@deck.gl/core/typed'; +import { MapboxOverlay } from '@deck.gl/mapbox/typed'; +import MapLibreCompare from '@maplibre/maplibre-gl-compare'; +import { TileLayer } from '@deck.gl/geo-layers/typed'; +import { CartoLayer, setDefaultCredentials, MAP_TYPES, API_VERSIONS } from '@deck.gl/carto/typed'; +import { useParams } from 'next/navigation'; +import { format } from 'date-fns'; +import bbox from '@turf/bbox'; + +import ZoomControl from './zoom'; +import LegendControl from './legend'; +import BasemapControl from './basemap'; + +import { useAppSelector } from '@/store/hooks'; +import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; +import { useEUDRData, usePlotGeometries } from '@/hooks/eudr'; +import { formatNumber } from '@/utils/number-format'; + +import type { MapboxOverlayProps } from '@deck.gl/mapbox/typed'; +import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; + +const monthFormatter = (date: string) => format(date, 'MM'); +const friendlyMonthFormatter = (date: string) => format(date, 'MMM'); + +const MAX_BOUNDS = [-75.76238126131099, -9.1712425377296, -74.4412398476887, -7.9871587484823845]; + +const DEFAULT_VIEW_STATE: MapViewState = { + ...INITIAL_VIEW_STATE, + latitude: -8.461844239054608, + longitude: -74.96226240479487, + zoom: 9, + minZoom: 7, + maxZoom: 20, +}; + +function DeckGLOverlay( + props: MapboxOverlayProps & { + interleaved?: boolean; + }, +) { + const overlay = useControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + return null; +} + +setDefaultCredentials({ + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjZDk0ZWIyZSJ9.oqLagnOEc-j7Z4hY-MTP1yoZA_vJ7WYYAkOz_NUmCJo', +}); + +const EUDRCompareMap: React.FC<{ supplierId?: string }> = ({ supplierId }) => { + const maps = useMap(); + + const { + planetLayer, + planetCompareLayer, + supplierLayer, + contextualLayers, + filters: { suppliers, materials, origins, plots, dates }, + table: { filters: tableFilters }, + } = useAppSelector((state) => state.eudr); + + const [hoverInfo, setHoverInfo] = useState(null); + const [viewState, setViewState] = useState(DEFAULT_VIEW_STATE); + + const params = useParams(); + + const { data } = useEUDRData( + { + startAlertDate: dates.from, + endAlertDate: dates.to, + producerIds: suppliers?.map(({ value }) => value), + materialIds: materials?.map(({ value }) => value), + originIds: origins?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), + }, + { + select: (data) => { + if (params?.supplierId) { + return { + dfs: data.table + .filter((row) => row.supplierId === (params.supplierId as string)) + .map((row) => row.plots.dfs.flat()) + .flat(), + sda: data.table + .filter((row) => row.supplierId === (params.supplierId as string)) + .map((row) => row.plots.sda.flat()) + .flat(), + }; + } + + const filteredData = data?.table.filter((dataRow) => { + if (Object.values(tableFilters).every((filter) => !filter)) return true; + + if (tableFilters.dfs && dataRow.dfs > 0) return true; + if (tableFilters.sda && dataRow.sda > 0) return true; + if (tableFilters.tpl && dataRow.tpl > 0) return true; + }); + + return { + dfs: filteredData.map((row) => row.plots.dfs.flat()).flat(), + sda: filteredData.map((row) => row.plots.sda.flat()).flat(), + }; + }, + }, + ); + + const plotGeometries = usePlotGeometries({ + producerIds: params?.supplierId + ? [params.supplierId as string] + : suppliers?.map(({ value }) => value), + materialIds: materials?.map(({ value }) => value), + originIds: origins?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), + }); + + const filteredGeometries: typeof plotGeometries.data = useMemo(() => { + if (!plotGeometries.data || !data) return null; + + if (params?.supplierId) return plotGeometries.data; + + return { + type: 'FeatureCollection', + features: plotGeometries.data.features.filter((feature) => { + if (Object.values(tableFilters).every((filter) => !filter)) return true; + + if (tableFilters.dfs && data.dfs.indexOf(feature.properties.id) > -1) return true; + if (tableFilters.sda && data.sda.indexOf(feature.properties.id) > -1) return true; + return false; + }), + }; + }, [data, plotGeometries.data, tableFilters, params]); + + const eudrSupplierLayer = useMemo(() => { + if (!filteredGeometries?.features || !data) return null; + + return new GeoJsonLayer<(typeof filteredGeometries)['features'][number]>({ + id: 'full-plots-layer', + // @ts-expect-error will fix this later... + data: filteredGeometries, + // Styles + filled: true, + getFillColor: ({ properties }) => { + if (data.dfs.indexOf(properties.id) > -1) return [74, 183, 243, 84]; + if (data.sda.indexOf(properties.id) > -1) return [255, 192, 56, 84]; + return [0, 0, 0, 84]; + }, + stroked: true, + getLineColor: ({ properties }) => { + if (data.dfs.indexOf(properties.id) > -1) return [74, 183, 243, 255]; + if (data.sda.indexOf(properties.id) > -1) return [255, 192, 56, 255]; + return [0, 0, 0, 84]; + }, + getLineWidth: 1, + lineWidthUnits: 'pixels', + // Interactive props + pickable: true, + autoHighlight: true, + highlightColor: (x: PickingInfo) => { + if (x.object?.properties?.id) { + const { + object: { + properties: { id }, + }, + } = x; + + if (data.dfs.indexOf(id) > -1) return [74, 183, 243, 255]; + if (data.sda.indexOf(id) > -1) return [255, 192, 56, 255]; + } + return [0, 0, 0, 84]; + }, + visible: supplierLayer.active, + onHover: setHoverInfo, + opacity: supplierLayer.opacity, + }); + }, [filteredGeometries, data, supplierLayer.active, supplierLayer.opacity]); + + const basemapPlanetLayer = new TileLayer({ + id: 'beforeMap-planet-monthly-layer', + data: `https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_${ + planetLayer.year + }_${monthFormatter( + planetLayer.month.toString(), + )}_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db`, + minZoom: 0, + maxZoom: 20, + tileSize: 256, + visible: planetLayer.active, + renderSubLayers: (props) => { + const { + bbox: { west, south, east, north }, + } = props.tile as { bbox: { west: number; south: number; east: number; north: number } }; + + return new BitmapLayer(props, { + data: null, + image: props.data, + bounds: [west, south, east, north], + }); + }, + }); + + const basemapPlanetCompareLayer = new TileLayer({ + id: 'afterMap-planet-monthly-layer', + data: `https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_${ + planetCompareLayer.year + }_${monthFormatter( + planetCompareLayer.month.toString(), + )}_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db`, + minZoom: 0, + maxZoom: 20, + tileSize: 256, + visible: planetCompareLayer.active, + renderSubLayers: (props) => { + const { + bbox: { west, south, east, north }, + } = props.tile as { bbox: { west: number; south: number; east: number; north: number } }; + + return new BitmapLayer(props, { + data: null, + image: props.data, + bounds: [west, south, east, north], + }); + }, + }); + + const forestCoverLayer = new CartoLayer({ + id: 'full-forest-cover-2020-ec-jrc', + type: MAP_TYPES.TILESET, + connection: 'eudr', + data: 'cartobq.eudr.JRC_2020_Forest_d_TILE', + stroked: false, + getFillColor: [114, 169, 80], + lineWidthMinPixels: 1, + opacity: contextualLayers['forest-cover-2020-ec-jrc'].opacity, + visible: contextualLayers['forest-cover-2020-ec-jrc'].active, + credentials: { + apiVersion: API_VERSIONS.V3, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjY2JlMjUyYSJ9.LoqzuDp076ESVYmHm1mZNtfhnqOVGmSxzp60Fht8PQw', + }, + }); + + const deforestationLayer = new CartoLayer({ + id: 'full-deforestation-alerts-2020-2022-hansen', + type: MAP_TYPES.QUERY, + connection: 'eudr', + data: 'SELECT * FROM `cartobq.eudr.TCL_hansen_year` WHERE year<=?', + queryParameters: [contextualLayers['deforestation-alerts-2020-2022-hansen'].year], + stroked: false, + getFillColor: [224, 191, 36], + lineWidthMinPixels: 1, + opacity: contextualLayers['deforestation-alerts-2020-2022-hansen'].opacity, + visible: contextualLayers['deforestation-alerts-2020-2022-hansen'].active, + credentials: { + apiVersion: API_VERSIONS.V3, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjZDk0ZWIyZSJ9.oqLagnOEc-j7Z4hY-MTP1yoZA_vJ7WYYAkOz_NUmCJo', + }, + }); + + const raddLayer = new CartoLayer({ + id: 'real-time-deforestation-alerts-since-2020-radd', + type: MAP_TYPES.QUERY, + connection: 'eudr', + data: 'SELECT * FROM `cartobq.eudr.RADD_date_confidence_3` WHERE date BETWEEN ? AND ?', + queryParameters: [ + contextualLayers['real-time-deforestation-alerts-since-2020-radd'].dateFrom, + contextualLayers['real-time-deforestation-alerts-since-2020-radd'].dateTo, + ], + stroked: false, + getFillColor: (d) => { + const { confidence } = d.properties; + if (confidence === 'Low') return [237, 164, 195]; + return [201, 42, 109]; + }, + lineWidthMinPixels: 1, + opacity: contextualLayers['real-time-deforestation-alerts-since-2020-radd'].opacity, + visible: contextualLayers['real-time-deforestation-alerts-since-2020-radd'].active, + credentials: { + apiVersion: API_VERSIONS.V3, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiI3NTFkNzA1YSJ9.jrVugV7HYfhmjxj-p2Iks8nL_AjHR91Q37JVP2fNmtc', + }, + }); + + 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]); + + useEffect(() => { + if (plotGeometries.data?.features?.length === 0 || plotGeometries.isLoading) { + return; + } + setTimeout(() => { + const newViewport = new WebMercatorViewport({ ...viewState, width: 800, height: 600 }); + const dataBounds = bbox(plotGeometries.data); + const newViewState = newViewport.fitBounds( + [ + [dataBounds[0], dataBounds[1]], + [dataBounds[2], dataBounds[3]], + ], + { + padding: 50, + }, + ); + const { latitude, longitude, zoom } = newViewState; + setViewState({ ...viewState, latitude, longitude, zoom }); + }, 100); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plotGeometries.data, plotGeometries.isLoading]); + + useEffect(() => { + if (!maps.afterMap || !maps.beforeMap) return; + const map = new MapLibreCompare(maps.beforeMap, maps.afterMap, '#comparison-container', { + orientation: 'horizontal', + }); + return () => map?.remove(); + }, [maps.afterMap, maps.beforeMap]); + + return ( + <> +
+ { + viewState.longitude = Math.min( + MAX_BOUNDS[2], + Math.max(MAX_BOUNDS[0], viewState.longitude), + ); + viewState.latitude = Math.min( + MAX_BOUNDS[3], + Math.max(MAX_BOUNDS[1], viewState.latitude), + ); + setViewState(viewState as MapViewState); + }} + controller={{ dragRotate: false }} + layers={[forestCoverLayer, deforestationLayer, raddLayer, eudrSupplierLayer]} + > + +
+ {`${planetLayer.year} ${friendlyMonthFormatter(planetLayer.month.toString())}`} +
+ +
+ +
+ {`${planetCompareLayer.year} ${friendlyMonthFormatter( + planetCompareLayer.month.toString(), + )}`} +
+ +
+
+
+ {hoverInfo?.object && ( +
+
+
+
Supplier:
+
{hoverInfo.object.properties.supplierName}
+
+
+
Plot:
+
{hoverInfo.object.properties.plotName}
+
+
+
Sourcing volume:
+
+ {formatNumber(hoverInfo.object.properties.baselineVolume)} t +
+
+
+
+ )} +
+ + + +
+ + ); +}; + +export default EUDRCompareMap; diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 7c6f3bfc0..0be434e93 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -1,10 +1,8 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import DeckGL from '@deck.gl/react/typed'; import { BitmapLayer, GeoJsonLayer } from '@deck.gl/layers/typed'; -import Map, { useMap, useControl } from 'react-map-gl/maplibre'; +import Map from 'react-map-gl/maplibre'; import { WebMercatorViewport } from '@deck.gl/core/typed'; -import { MapboxOverlay } from '@deck.gl/mapbox/typed'; -import MapLibreCompare from '@maplibre/maplibre-gl-compare'; import { TileLayer } from '@deck.gl/geo-layers/typed'; import { CartoLayer, setDefaultCredentials, MAP_TYPES, API_VERSIONS } from '@deck.gl/carto/typed'; import { useParams } from 'next/navigation'; @@ -20,7 +18,6 @@ import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; import { useEUDRData, usePlotGeometries } from '@/hooks/eudr'; import { formatNumber } from '@/utils/number-format'; -import type { MapboxOverlayProps } from '@deck.gl/mapbox/typed'; import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; const monthFormatter = (date: string) => format(date, 'MM'); @@ -36,28 +33,15 @@ const DEFAULT_VIEW_STATE: MapViewState = { maxZoom: 20, }; -function DeckGLOverlay( - props: MapboxOverlayProps & { - interleaved?: boolean; - }, -) { - const overlay = useControl(() => new MapboxOverlay(props)); - overlay.setProps(props); - return null; -} - setDefaultCredentials({ apiBaseUrl: 'https://gcp-us-east1.api.carto.com', accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjZDk0ZWIyZSJ9.oqLagnOEc-j7Z4hY-MTP1yoZA_vJ7WYYAkOz_NUmCJo', }); -const EUDRMap: React.FC<{ supplierId: string }> = ({ supplierId }) => { - const maps = useMap(); - +const EUDRMap: React.FC<{ supplierId?: string }> = ({ supplierId }) => { const { planetLayer, - planetCompareLayer, supplierLayer, contextualLayers, filters: { suppliers, materials, origins, plots, dates }, @@ -203,30 +187,6 @@ const EUDRMap: React.FC<{ supplierId: string }> = ({ supplierId }) => { }, }); - const basemapPlanetCompareLayer = new TileLayer({ - id: 'afterMap-planet-monthly-layer', - data: `https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_${ - planetCompareLayer.year - }_${monthFormatter( - planetCompareLayer.month.toString(), - )}_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db`, - minZoom: 0, - maxZoom: 20, - tileSize: 256, - visible: planetCompareLayer.active, - renderSubLayers: (props) => { - const { - bbox: { west, south, east, north }, - } = props.tile as { bbox: { west: number; south: number; east: number; north: number } }; - - return new BitmapLayer(props, { - data: null, - image: props.data, - bounds: [west, south, east, north], - }); - }, - }); - const forestCoverLayer = new CartoLayer({ id: 'full-forest-cover-2020-ec-jrc', type: MAP_TYPES.TILESET, @@ -304,7 +264,7 @@ const EUDRMap: React.FC<{ supplierId: string }> = ({ supplierId }) => { if (!supplierId || plotGeometries.data?.features?.length === 0 || plotGeometries.isLoading) { return; } - const newViewport = new WebMercatorViewport({ ...viewState }); + const newViewport = new WebMercatorViewport({ ...viewState, width: 800, height: 600 }); const dataBounds = bbox(plotGeometries.data); setTimeout(() => { const newViewState = newViewport.fitBounds( @@ -322,49 +282,33 @@ const EUDRMap: React.FC<{ supplierId: string }> = ({ supplierId }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [plotGeometries.data, plotGeometries.isLoading, supplierId]); - useEffect(() => { - if (!planetCompareLayer.active || !maps.afterMap || !maps.beforeMap) return; - const map = new MapLibreCompare(maps.beforeMap, maps.afterMap, '#comparison-container', { - orientation: 'horizontal', - }); - return () => map?.remove(); - }, [maps.afterMap, maps.beforeMap, planetCompareLayer.active]); - return ( <> -
+
{ - // viewState.longitude = Math.min( - // MAX_BOUNDS[2], - // Math.max(MAX_BOUNDS[0], viewState.longitude), - // ); - // viewState.latitude = Math.min( - // MAX_BOUNDS[3], - // Math.max(MAX_BOUNDS[1], viewState.latitude), - // ); + viewState.longitude = Math.min( + MAX_BOUNDS[2], + Math.max(MAX_BOUNDS[0], viewState.longitude), + ); + viewState.latitude = Math.min( + MAX_BOUNDS[3], + Math.max(MAX_BOUNDS[1], viewState.latitude), + ); setViewState(viewState as MapViewState); }} controller={{ dragRotate: false }} - layers={[forestCoverLayer, deforestationLayer, raddLayer, eudrSupplierLayer]} + layers={[ + ...(planetLayer.active ? [basemapPlanetLayer] : []), + forestCoverLayer, + deforestationLayer, + raddLayer, + eudrSupplierLayer, + ]} > - {!planetCompareLayer.active && ( - - {planetLayer.active && } - - )} - {planetCompareLayer.active && ( - - - - )} - {planetCompareLayer.active && ( - - - - )} +
{hoverInfo?.object && ( diff --git a/client/src/pages/eudr/index.tsx b/client/src/pages/eudr/index.tsx index 2cff92e3e..e67c40206 100644 --- a/client/src/pages/eudr/index.tsx +++ b/client/src/pages/eudr/index.tsx @@ -7,6 +7,7 @@ import { tasksSSR } from 'services/ssr'; import ApplicationLayout from 'layouts/application'; import CollapseButton from 'containers/collapse-button/component'; import TitleTemplate from 'utils/titleTemplate'; +import { useAppSelector } from '@/store/hooks'; import SuppliersStackedBar from '@/containers/analysis-eudr/suppliers-stacked-bar'; import EUDRFilters from '@/containers/analysis-eudr/filters/component'; import SupplierListTable from '@/containers/analysis-eudr/supplier-list-table'; @@ -19,8 +20,13 @@ const DynamicMap = dynamic(() => import('containers/analysis-eudr/map'), { ssr: false, }); +const DynamicCompareMap = dynamic(() => import('containers/analysis-eudr/map/compare'), { + ssr: false, +}); + const MapPage: NextPageWithLayout = () => { const scrollRef = useRef(null); + const { planetCompareLayer } = useAppSelector((state) => state.eudr); const [isCollapsed, setIsCollapsed] = useState(false); return ( @@ -57,7 +63,8 @@ const MapPage: NextPageWithLayout = () => {
- + {!planetCompareLayer.active && } + {planetCompareLayer.active && }
diff --git a/client/src/pages/eudr/suppliers/[supplierId].tsx b/client/src/pages/eudr/suppliers/[supplierId].tsx index 8c03a6ef6..01783aac6 100644 --- a/client/src/pages/eudr/suppliers/[supplierId].tsx +++ b/client/src/pages/eudr/suppliers/[supplierId].tsx @@ -28,9 +28,14 @@ const DynamicMap = dynamic(() => import('containers/analysis-eudr/map'), { ssr: false, }); +const DynamicCompareMap = dynamic(() => import('containers/analysis-eudr/map/compare'), { + ssr: false, +}); + const MapPage: NextPageWithLayout = () => { const scrollRef = useRef(null); const [isCollapsed, setIsCollapsed] = useState(false); + const { planetCompareLayer } = useAppSelector((state) => state.eudr); const { filters: { dates }, } = useAppSelector(eudrDetail); @@ -91,7 +96,8 @@ const MapPage: NextPageWithLayout = () => {
- + {!planetCompareLayer.active && } + {planetCompareLayer.active && }