Skip to content

Commit

Permalink
Adding capability for CSV + GeoJSON download of composite data; COUNT…
Browse files Browse the repository at this point in the history
…RY=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
  • Loading branch information
gislawill authored Sep 13, 2024
1 parent 763871b commit 326be39
Showing 1 changed file with 132 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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<AdminLevelDataLayerProps>;
const layerData = useSelector(
layerDataSelector(layer.id, requestDate),
) as LayerData<AdminLevelDataLayerProps | CompositeLayerProps>;

const handleDownloadMenuClose = () => {
setDownloadMenuAnchorEl(null);
Expand All @@ -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<AdminLevelDataLayerProps>)?.data.layerData[0],
(_v, k) => (k === 'value' ? t(layerData.layer.id) : t(k)),
);
downloadToFile(
{
content: castObjectsArrayToCsv(
(layerData as LayerData<AdminLevelDataLayerProps>)?.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<CompositeLayerProps>;
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;
Expand All @@ -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, '&lt;').replace(/>/g, '&gt;');

// Helper function to generate QML content from legend
const generateQmlContent = (
legend: LegendDefinitionItem[],
opacity: number = 1,
scalingFactor: number = 1,
): string => {
let qml = `<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
const generateQmlContent = useCallback(
(
legend: LegendDefinitionItem[],
opacity: number = 1,
scalingFactor: number = 1,
): string => {
let qml = `<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis hasScaleBasedVisibilityFlag="0" styleCategories="AllStyleCategories">
<pipe>
<rasterrenderer opacity="${opacity}" alphaBand="-1" band="1" classificationMin="-1" classificationMax="inf" type="singlebandpseudocolor">
<rasterTransparency />
<rastershader>
<colorrampshader colorRampType="DISCRETE" classificationMode="1" clip="0">`;
// 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 += `
<item color="${item.color}" value="${value}" alpha="255" label="${label}" />`;
});
});

// 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 += `
</colorrampshader>
</rastershader>
</rasterrenderer>
</pipe>
</qgis>`;

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);
Expand All @@ -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'));

Expand Down Expand Up @@ -247,7 +297,7 @@ function LayerDownloadOptions({
open={Boolean(downloadMenuAnchorEl)}
onClose={handleDownloadMenuClose}
>
{layer.type === 'admin_level_data' && [
{(layer.type === 'admin_level_data' || layer.type === 'composite') && [
<MenuItem key="download-as-csv" onClick={handleDownloadCsv}>
{t('Download as CSV')}
</MenuItem>,
Expand Down

0 comments on commit 326be39

Please sign in to comment.