Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding capability for CSV + GeoJSON download of composite data; COUNTRY=jordan #1339

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading