From 326be39ab483dd45e93b7ec8b903417178d6c227 Mon Sep 17 00:00:00 2001 From: Will Gislason <8203830+gislawill@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:24:36 -0700 Subject: [PATCH] Adding capability for CSV + GeoJSON download of composite data; COUNTRY=jordan (#1339) * Adding capability for CSV + Geojson download of composite data * Fixing geojson downloads * Check the request date item to handle date ranges * Fixing csv downloads * Clean up types a bit, add some callbacks, add country name to file download * update file download name --- .../SwitchItem/LayerDownloadOptions.tsx | 214 +++++++++++------- 1 file changed, 132 insertions(+), 82 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..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 @@ -6,12 +6,13 @@ import { MenuItem, Tooltip, } from '@material-ui/core'; -import React, { 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'; import { AdminLevelDataLayerProps, + CompositeLayerProps, LayerKey, LegendDefinitionItem, WMSLayerProps, @@ -30,10 +31,11 @@ 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'; +import { Point } from 'geojson'; // TODO - return early when the layer is not selected. function LayerDownloadOptions({ @@ -56,13 +58,15 @@ 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), - ) as LayerData; + const layerData = useSelector( + layerDataSelector(layer.id, requestDate), + ) as LayerData; const handleDownloadMenuClose = () => { setDownloadMenuAnchorEl(null); @@ -72,54 +76,96 @@ function LayerDownloadOptions({ setDownloadMenuAnchorEl(event.currentTarget); }; - const getFilename = (): string => { + 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 `${safeTitle}_${dateString}`; + return `${safeCountry}_${safeTitle}_${dateString}`; } return safeTitle; - }; + }, [layer, selectedDate]); const handleDownloadGeoJson = (): void => { - if (!adminLevelLayerData) { - console.warn(`No layer data available for ${layer.id}`); + if (layerData) { + const { features } = layerData.data; + downloadToFile( + { + content: JSON.stringify({ + type: 'FeatureCollection', + 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}`); + const handleDownloadCsv = useCallback(() => { + if (layerData && layer.type === 'admin_level_data') { + const translatedColumnsNames = mapValues( + (layerData as LayerData)?.data.layerData[0], + (_v, k) => (k === 'value' ? t(layerData.layer.id) : t(k)), + ); + downloadToFile( + { + content: castObjectsArrayToCsv( + (layerData as LayerData)?.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 (layerData && layer.type === 'composite') { + const compositeLayerData = layerData as LayerData; + 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(layerData.layer.id) : t(k)), + ); - const handleDownloadGeoTiff = (): void => { + downloadToFile( + { + content: castObjectsArrayToCsv( + csvData, + translatedColumnsNames, + ';', + ), + isUrl: false, + }, + getFilename(), + 'text/csv', + ); + handleDownloadMenuClose(); + } + } + console.warn(`No layer data available for ${layer.id}`); + }, [layerData, layer.type, layer.id, getFilename, t]); + + const handleDownloadGeoTiff = useCallback(() => { const { serverLayerName, additionalQueryParams } = layer as WMSLayerProps; const band = getStacBand(additionalQueryParams); const dateString = getFormattedDate(selectedDate, 'default') as string; @@ -135,64 +181,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); @@ -207,10 +256,11 @@ function LayerDownloadOptions({ ); handleDownloadMenuClose(); - }; + }, [layer, generateQmlContent, layerId]); const shouldShowDownloadButton = layer.type === 'admin_level_data' || + layer.type === 'composite' || (layer.type === 'wms' && layer.baseUrl.includes('api.earthobservation.vam.wfp.org/ows')); @@ -247,7 +297,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')} ,