From 3146fea41649b84c8858a2d523eb9f0de458ced2 Mon Sep 17 00:00:00 2001 From: Will Gislason <8203830+gislawill@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:33:47 -0700 Subject: [PATCH 1/6] Adding capability for CSV + Geojson download of composite data --- .../SwitchItem/LayerDownloadOptions.tsx | 103 ++++++++++++------ 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx index 87a6c47e1..cafa4cbbc 100644 --- a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx +++ b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx @@ -12,6 +12,7 @@ import { mapValues } from 'lodash'; import GetAppIcon from '@material-ui/icons/GetApp'; import { AdminLevelDataLayerProps, + CompositeLayerProps, LayerKey, LegendDefinitionItem, WMSLayerProps, @@ -34,6 +35,7 @@ import { getRequestDate } from 'utils/server-utils'; import { LayerDefinitions, getStacBand } from 'config/utils'; import { getFormattedDate } from 'utils/date-utils'; import { safeCountry } from 'config'; +import { Point } from 'geojson'; // TODO - return early when the layer is not selected. function LayerDownloadOptions({ @@ -63,6 +65,9 @@ function LayerDownloadOptions({ const adminLevelLayerData = useSelector( layerDataSelector(layer.id, queryDate), ) as LayerData; + const compositeLayerData = useSelector( + layerDataSelector(layer.id), + ) as LayerData; const handleDownloadMenuClose = () => { setDownloadMenuAnchorEl(null); @@ -82,41 +87,76 @@ function LayerDownloadOptions({ }; const handleDownloadGeoJson = (): void => { - if (!adminLevelLayerData) { - console.warn(`No layer data available for ${layer.id}`); + if (adminLevelLayerData || compositeLayerData) { + const features = + adminLevelLayerData?.data.features || compositeLayerData?.data.features; + downloadToFile( + { + content: JSON.stringify(features), + isUrl: false, + }, + getFilename(), + 'application/json', + ); + handleDownloadMenuClose(); + return; } - downloadToFile( - { - content: JSON.stringify(adminLevelLayerData?.data.features), - isUrl: false, - }, - getFilename(), - 'application/json', - ); - handleDownloadMenuClose(); + console.warn(`No layer data available for ${layer.id}`); }; const handleDownloadCsv = (): void => { - if (!adminLevelLayerData) { - console.warn(`No layer data available for ${layer.id}`); + if (adminLevelLayerData) { + const translatedColumnsNames = mapValues( + adminLevelLayerData?.data.layerData[0], + (_v, k) => (k === 'value' ? t(adminLevelLayerData.layer.id) : t(k)), + ); + downloadToFile( + { + content: castObjectsArrayToCsv( + adminLevelLayerData?.data.layerData, + translatedColumnsNames, + ';', + ), + isUrl: false, + }, + getFilename(), + 'text/csv', + ); + handleDownloadMenuClose(); } - const translatedColumnsNames = mapValues( - adminLevelLayerData?.data.layerData[0], - (_v, k) => (k === 'value' ? t(adminLevelLayerData.layer.id) : t(k)), - ); - downloadToFile( - { - content: castObjectsArrayToCsv( - adminLevelLayerData?.data.layerData, - translatedColumnsNames, - ';', - ), - isUrl: false, - }, - getFilename(), - 'text/csv', - ); - handleDownloadMenuClose(); + if (compositeLayerData) { + const geoJsonFeatures = compositeLayerData?.data.features; + const properties = geoJsonFeatures[0]?.properties; + + if (properties) { + // Add coordinates to each feature's properties + const csvData = geoJsonFeatures.map(feature => ({ + ...feature.properties, + coordinates: (feature.geometry as Point).coordinates.join(', '), + })); + + // Translate column names and set "value" to layer.id + const translatedColumnsNames = mapValues( + { coordinates: 'coordinates', ...properties }, + (_v, k) => (k === 'value' ? t(compositeLayerData.layer.id) : t(k)), + ); + + downloadToFile( + { + content: castObjectsArrayToCsv( + csvData, + translatedColumnsNames, + ';', + ), + isUrl: false, + }, + getFilename(), + 'text/csv', + ); + handleDownloadMenuClose(); + } + } + console.warn(`No layer data available for ${layer.id}`); }; const handleDownloadGeoTiff = (): void => { @@ -211,6 +251,7 @@ function LayerDownloadOptions({ const shouldShowDownloadButton = layer.type === 'admin_level_data' || + layer.type === 'composite' || (layer.type === 'wms' && layer.baseUrl.includes('api.earthobservation.vam.wfp.org/ows')); @@ -247,7 +288,7 @@ function LayerDownloadOptions({ open={Boolean(downloadMenuAnchorEl)} onClose={handleDownloadMenuClose} > - {layer.type === 'admin_level_data' && [ + {(layer.type === 'admin_level_data' || layer.type === 'composite') && [ {t('Download as CSV')} , From d1295b33822a862966d42b99706c434bf04e3ea8 Mon Sep 17 00:00:00 2001 From: Will Gislason <8203830+gislawill@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:52:50 -0700 Subject: [PATCH 2/6] Fixing geojson downloads --- .../MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx index cafa4cbbc..6e1324a75 100644 --- a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx +++ b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx @@ -92,7 +92,10 @@ function LayerDownloadOptions({ adminLevelLayerData?.data.features || compositeLayerData?.data.features; downloadToFile( { - content: JSON.stringify(features), + content: JSON.stringify({ + type: 'FeatureCollection', + features, + }), isUrl: false, }, getFilename(), From 68a65306ba3facde96db15e14e1757fcb15a7e1f Mon Sep 17 00:00:00 2001 From: Will Gislason <8203830+gislawill@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:45:35 -0700 Subject: [PATCH 3/6] Check the request date item to handle date ranges --- .../SwitchItem/LayerDownloadOptions.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx index 6e1324a75..1dc84b754 100644 --- a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx +++ b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx @@ -6,7 +6,7 @@ import { MenuItem, Tooltip, } from '@material-ui/core'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { mapValues } from 'lodash'; import GetAppIcon from '@material-ui/icons/GetApp'; @@ -31,7 +31,7 @@ import { import { useSafeTranslation } from 'i18n'; import { isExposureAnalysisLoadingSelector } from 'context/analysisResultStateSlice'; import { availableDatesSelector } from 'context/serverStateSlice'; -import { getRequestDate } from 'utils/server-utils'; +import { getRequestDateItem } from 'utils/server-utils'; import { LayerDefinitions, getStacBand } from 'config/utils'; import { getFormattedDate } from 'utils/date-utils'; import { safeCountry } from 'config'; @@ -58,15 +58,17 @@ function LayerDownloadOptions({ const { startDate: selectedDate } = useSelector(dateRangeSelector); const serverAvailableDates = useSelector(availableDatesSelector); const layerAvailableDates = serverAvailableDates[layer.id]; - const queryDate = selected - ? getRequestDate(layerAvailableDates, selectedDate) - : undefined; + const queryDateItem = useMemo( + () => getRequestDateItem(layerAvailableDates, selectedDate, false), + [layerAvailableDates, selectedDate], + ); + const requestDate = queryDateItem?.startDate || queryDateItem?.queryDate; const adminLevelLayerData = useSelector( - layerDataSelector(layer.id, queryDate), + layerDataSelector(layer.id, requestDate), ) as LayerData; const compositeLayerData = useSelector( - layerDataSelector(layer.id), + layerDataSelector(layer.id, requestDate), ) as LayerData; const handleDownloadMenuClose = () => { From f50693ca5497d88d071b2189aa04d351e8e16d60 Mon Sep 17 00:00:00 2001 From: Will Gislason <8203830+gislawill@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:39:06 -0700 Subject: [PATCH 4/6] Fixing csv downloads --- .../MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx index 1dc84b754..579838e3a 100644 --- a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx +++ b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx @@ -110,7 +110,7 @@ function LayerDownloadOptions({ }; const handleDownloadCsv = (): void => { - if (adminLevelLayerData) { + if (adminLevelLayerData && layer.type === 'admin_level_data') { const translatedColumnsNames = mapValues( adminLevelLayerData?.data.layerData[0], (_v, k) => (k === 'value' ? t(adminLevelLayerData.layer.id) : t(k)), @@ -129,7 +129,7 @@ function LayerDownloadOptions({ ); handleDownloadMenuClose(); } - if (compositeLayerData) { + if (compositeLayerData && layer.type === 'composite') { const geoJsonFeatures = compositeLayerData?.data.features; const properties = geoJsonFeatures[0]?.properties; From ce346114d3370f531689e71b15438a6b48d3b74f Mon Sep 17 00:00:00 2001 From: Will Gislason <8203830+gislawill@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:42:02 -0700 Subject: [PATCH 5/6] Clean up types a bit, add some callbacks, add country name to file download --- .../SwitchItem/LayerDownloadOptions.tsx | 115 +++++++++--------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx index 579838e3a..d08ae2a6f 100644 --- a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx +++ b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx @@ -6,7 +6,7 @@ import { MenuItem, Tooltip, } from '@material-ui/core'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { mapValues } from 'lodash'; import GetAppIcon from '@material-ui/icons/GetApp'; @@ -64,12 +64,9 @@ function LayerDownloadOptions({ ); const requestDate = queryDateItem?.startDate || queryDateItem?.queryDate; - const adminLevelLayerData = useSelector( + const layerData = useSelector( layerDataSelector(layer.id, requestDate), - ) as LayerData; - const compositeLayerData = useSelector( - layerDataSelector(layer.id, requestDate), - ) as LayerData; + ) as LayerData; const handleDownloadMenuClose = () => { setDownloadMenuAnchorEl(null); @@ -79,19 +76,18 @@ function LayerDownloadOptions({ setDownloadMenuAnchorEl(event.currentTarget); }; - const getFilename = (): string => { + const getFilename = useCallback((): string => { const safeTitle = layer.title ?? layer.id; if (selectedDate && (layer as AdminLevelDataLayerProps).dates) { const dateString = getFormattedDate(selectedDate, 'snake'); - return `${safeTitle}_${dateString}`; + return `${safeCountry}_${safeTitle}_${dateString}`; } return safeTitle; - }; + }, [layer, selectedDate]); const handleDownloadGeoJson = (): void => { - if (adminLevelLayerData || compositeLayerData) { - const features = - adminLevelLayerData?.data.features || compositeLayerData?.data.features; + if (layerData) { + const { features } = layerData.data; downloadToFile( { content: JSON.stringify({ @@ -109,16 +105,16 @@ function LayerDownloadOptions({ console.warn(`No layer data available for ${layer.id}`); }; - const handleDownloadCsv = (): void => { - if (adminLevelLayerData && layer.type === 'admin_level_data') { + const handleDownloadCsv = useCallback(() => { + if (layerData && layer.type === 'admin_level_data') { const translatedColumnsNames = mapValues( - adminLevelLayerData?.data.layerData[0], - (_v, k) => (k === 'value' ? t(adminLevelLayerData.layer.id) : t(k)), + (layerData as LayerData)?.data.layerData[0], + (_v, k) => (k === 'value' ? t(layerData.layer.id) : t(k)), ); downloadToFile( { content: castObjectsArrayToCsv( - adminLevelLayerData?.data.layerData, + (layerData as LayerData)?.data.layerData, translatedColumnsNames, ';', ), @@ -129,7 +125,9 @@ function LayerDownloadOptions({ ); handleDownloadMenuClose(); } - if (compositeLayerData && layer.type === 'composite') { + if (layerData && layer.type === 'composite') { + // set layerData as LayerData + const compositeLayerData = layerData as LayerData; const geoJsonFeatures = compositeLayerData?.data.features; const properties = geoJsonFeatures[0]?.properties; @@ -143,7 +141,7 @@ function LayerDownloadOptions({ // Translate column names and set "value" to layer.id const translatedColumnsNames = mapValues( { coordinates: 'coordinates', ...properties }, - (_v, k) => (k === 'value' ? t(compositeLayerData.layer.id) : t(k)), + (_v, k) => (k === 'value' ? t(layerData.layer.id) : t(k)), ); downloadToFile( @@ -162,9 +160,9 @@ function LayerDownloadOptions({ } } console.warn(`No layer data available for ${layer.id}`); - }; + }, [layerData, layer.type, layer.id, getFilename, t]); - const handleDownloadGeoTiff = (): void => { + const handleDownloadGeoTiff = useCallback(() => { const { serverLayerName, additionalQueryParams } = layer as WMSLayerProps; const band = getStacBand(additionalQueryParams); const dateString = getFormattedDate(selectedDate, 'default') as string; @@ -180,64 +178,67 @@ function LayerDownloadOptions({ () => setIsGeotiffLoading(false), ); handleDownloadMenuClose(); - }; + }, [layer, selectedDate, extent, layerId, dispatch]); // Helper function to escape special XML characters const escapeXml = (str: string): string => str.replace(//g, '>'); // Helper function to generate QML content from legend - const generateQmlContent = ( - legend: LegendDefinitionItem[], - opacity: number = 1, - scalingFactor: number = 1, - ): string => { - let qml = ` + const generateQmlContent = useCallback( + ( + legend: LegendDefinitionItem[], + opacity: number = 1, + scalingFactor: number = 1, + ): string => { + let qml = ` `; - // Add color entries for each legend item - legend.forEach((item, index) => { - const label = item.label - ? escapeXml(item.label as string) - : item.value.toString(); + // Add color entries for each legend item + legend.forEach((item, index) => { + const label = item.label + ? escapeXml(item.label as string) + : item.value.toString(); - // TEMPORARY: shift the value index by 1 to account for the 0 value - // and match the QML style format. See https://github.com/WFP-VAM/prism-app/pull/1161 - const shouldShiftIndex = - (legend[0].value === 0 || - (legend[0].label as string).includes('< -')) && - ((legend[0].label as string)?.includes('<') || - (legend[1].label as string)?.includes('<') || - (legend[1].label as string)?.includes('-') || - (legend[1].label as string)?.includes(' to ')); + // TEMPORARY: shift the value index by 1 to account for the 0 value + // and match the QML style format. See https://github.com/WFP-VAM/prism-app/pull/1161 + const shouldShiftIndex = + (legend[0].value === 0 || + (legend[0].label as string).includes('< -')) && + ((legend[0].label as string)?.includes('<') || + (legend[1].label as string)?.includes('<') || + (legend[1].label as string)?.includes('-') || + (legend[1].label as string)?.includes(' to ')); - const value = - index < legend.length - 1 - ? (legend[index + Number(shouldShiftIndex)]?.value as number) * - scalingFactor - : 'INF'; - // eslint-disable-next-line fp/no-mutation - qml += ` + const value = + index < legend.length - 1 + ? (legend[index + Number(shouldShiftIndex)]?.value as number) * + scalingFactor + : 'INF'; + // eslint-disable-next-line fp/no-mutation + qml += ` `; - }); + }); - // End of QML file content - // eslint-disable-next-line fp/no-mutation - qml += ` + // End of QML file content + // eslint-disable-next-line fp/no-mutation + qml += ` `; - return qml; - }; + return qml; + }, + [], + ); - const handleDownloadQmlStyle = (): void => { + const handleDownloadQmlStyle = useCallback((): void => { const { legend, opacity, wcsConfig } = layer as WMSLayerProps; const scalingFactor = wcsConfig?.scale ? 1 / Number(wcsConfig.scale) : 1; const qmlContent = generateQmlContent(legend, opacity, scalingFactor); @@ -252,7 +253,7 @@ function LayerDownloadOptions({ ); handleDownloadMenuClose(); - }; + }, [layer, generateQmlContent, layerId]); const shouldShowDownloadButton = layer.type === 'admin_level_data' || From b23d2199aa37a1edc3a3fec9cfb39d68db6535dd Mon Sep 17 00:00:00 2001 From: Will Gislason <8203830+gislawill@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:48:17 -0700 Subject: [PATCH 6/6] update file download name --- .../MenuSwitch/SwitchItem/LayerDownloadOptions.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx index d08ae2a6f..a15517db6 100644 --- a/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx +++ b/frontend/src/components/MapView/LeftPanel/layersPanel/MenuItem/MenuSwitch/SwitchItem/LayerDownloadOptions.tsx @@ -78,7 +78,11 @@ function LayerDownloadOptions({ const getFilename = useCallback((): string => { const safeTitle = layer.title ?? layer.id; - if (selectedDate && (layer as AdminLevelDataLayerProps).dates) { + if ( + selectedDate && + ((layer as AdminLevelDataLayerProps).dates || + (layer as CompositeLayerProps).dateLayer) + ) { const dateString = getFormattedDate(selectedDate, 'snake'); return `${safeCountry}_${safeTitle}_${dateString}`; } @@ -126,7 +130,6 @@ function LayerDownloadOptions({ handleDownloadMenuClose(); } if (layerData && layer.type === 'composite') { - // set layerData as LayerData const compositeLayerData = layerData as LayerData; const geoJsonFeatures = compositeLayerData?.data.features; const properties = geoJsonFeatures[0]?.properties;