Vizzuality/landgriffon

View on GitHub
client/src/containers/analysis-eudr/map/component.tsx

Summary

Maintainability
F
1 wk
Test Coverage
import { useState, useCallback, useEffect, useMemo } from 'react';
import DeckGL from '@deck.gl/react/typed';
import { GeoJsonLayer } from '@deck.gl/layers/typed';
import Map, { Source, Layer } from 'react-map-gl/maplibre';
import { WebMercatorViewport } from '@deck.gl/core/typed';
import { CartoLayer, 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 { env } from '@/env.mjs';

import type { PickingInfo, MapViewState } from '@deck.gl/core/typed';

const monthFormatter = (date: string) => format(date, 'MM');

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,
};

const EUDRMap: React.FC<{ supplierId?: string }> = ({ supplierId }) => {
  const {
    planetLayer,
    supplierLayer,
    contextualLayers,
    filters: { suppliers, materials, origins, plots, dates },
    table: { filters: tableFilters },
  } = useAppSelector((state) => state.eudr);

  const [hoverInfo, setHoverInfo] = useState<PickingInfo>(null);
  const [viewState, setViewState] = useState<MapViewState>(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 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: env.NEXT_PUBLIC_CARTO_FOREST_ACCESS_TOKEN,
    },
  });

  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: env.NEXT_PUBLIC_CARTO_DEFORESTATION_ACCESS_TOKEN,
    },
  });

  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: env.NEXT_PUBLIC_CARTO_RADD_ACCESS_TOKEN,
    },
  });

  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 (!supplierId || plotGeometries.data?.features?.length === 0 || plotGeometries.isLoading) {
      return;
    }
    const newViewport = new WebMercatorViewport({ ...viewState, width: 800, height: 600 });
    const dataBounds = bbox(plotGeometries.data);
    setTimeout(() => {
      const newViewState = newViewport.fitBounds(
        [
          [dataBounds[0], dataBounds[1]],
          [dataBounds[2], dataBounds[3]],
        ],
        {
          padding: 50,
        },
      );
      const { latitude, longitude, zoom } = newViewState;
      setViewState({ ...viewState, latitude, longitude, zoom });
    }, 160);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [plotGeometries.data, plotGeometries.isLoading, supplierId]);

  return (
    <>
      <div className="absolute left-0 top-0 h-full w-full">
        <DeckGL
          id="anotherDeckMap"
          viewState={{ ...viewState }}
          onViewStateChange={({ viewState }) => {
            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]}
        >
          <Map id="mainMap" reuseMaps mapStyle={MAP_STYLES.terrain} styleDiffing={false}>
            {planetLayer.active && (
              <Source
                type="raster"
                tiles={[
                  `https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_${
                    planetLayer.year
                  }_${monthFormatter(
                    planetLayer.month.toString(),
                  )}_mosaic/gmap/{z}/{x}/{y}.png?api_key=${env.NEXT_PUBLIC_PLANET_API_KEY}`,
                ]}
                tileSize={256}
              >
                <Layer id="monthlyPlanetLAyer" type="raster" />
              </Source>
            )}
          </Map>
        </DeckGL>
      </div>
      {hoverInfo?.object && (
        <div
          className="pointer-events-none absolute z-10 max-w-32 rounded-md bg-white p-2 text-2xs shadow-md"
          style={{
            left: hoverInfo?.x + 10,
            top: hoverInfo?.y + 10,
          }}
        >
          <dl className="space-y-2">
            <div>
              <dt>Supplier: </dt>
              <dd className="font-semibold">{hoverInfo.object.properties.supplierName}</dd>
            </div>
            <div>
              <dt>Plot: </dt>
              <dd className="font-semibold">{hoverInfo.object.properties.plotName}</dd>
            </div>
            <div>
              <dt>Sourcing volume: </dt>
              <dd className="font-semibold">
                {formatNumber(hoverInfo.object.properties.baselineVolume)} t
              </dd>
            </div>
          </dl>
        </div>
      )}
      <div className="absolute bottom-10 right-6 z-10 w-10 space-y-2">
        <BasemapControl />
        <ZoomControl viewState={viewState} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} />
        <LegendControl />
      </div>
    </>
  );
};

export default EUDRMap;