From b7e3e0369c087d9a1e2bfa475730d7d23e8d892d Mon Sep 17 00:00:00 2001 From: Luis Zenteno Date: Mon, 21 Oct 2024 17:31:51 -0600 Subject: [PATCH] feat(natural-forest): natural forest widget epic --- .../tree-loss-plantations/index.js | 53 +++---- .../tree-loss-plantations/selectors.js | 79 +++++----- .../land-cover/natural-forest/index.js | 117 ++++++++++++++ .../land-cover/natural-forest/selectors.js | 126 +++++++++++++++ .../land-cover/tree-cover-ranked/index.js | 2 +- components/widgets/manifest.js | 2 + components/widgets/utils/config.js | 4 + components/widgets/utils/data.js | 22 +++ data/datasets.js | 1 + data/layers.js | 1 + services/analysis-cached.js | 144 ++++++++++++++++++ services/get-where-query.js | 10 +- services/sentences.js | 102 +++++++++++-- 13 files changed, 575 insertions(+), 88 deletions(-) create mode 100644 components/widgets/land-cover/natural-forest/index.js create mode 100644 components/widgets/land-cover/natural-forest/selectors.js diff --git a/components/widgets/forest-change/tree-loss-plantations/index.js b/components/widgets/forest-change/tree-loss-plantations/index.js index 626c529957..0c15df324f 100644 --- a/components/widgets/forest-change/tree-loss-plantations/index.js +++ b/components/widgets/forest-change/tree-loss-plantations/index.js @@ -1,22 +1,22 @@ import { all, spread } from 'axios'; -import { getLoss } from 'services/analysis-cached'; +import { getLossNaturalForest } from 'services/analysis-cached'; import { getYearsRangeFromMinMax } from 'components/widgets/utils/data'; import { POLITICAL_BOUNDARIES_DATASET, FOREST_LOSS_DATASET, - TREE_PLANTATIONS_DATASET, + NATURAL_FOREST, } from 'data/datasets'; import { DISPUTED_POLITICAL_BOUNDARIES, POLITICAL_BOUNDARIES, FOREST_LOSS, - TREE_PLANTATIONS, + NATURAL_FOREST_2020, } from 'data/layers'; import getWidgetProps from './selectors'; -const MIN_YEAR = 2013; +const MIN_YEAR = 2021; const MAX_YEAR = 2023; export default { @@ -27,6 +27,12 @@ export default { subcategories: ['forest-loss'], types: ['country', 'aoi', 'wdpa'], admins: ['adm0', 'adm1', 'adm2'], + alerts: [ + { + text: 'Not all natural forest area can be monitored with existing data on tree cover loss. See the metadata for more information.', + visible: ['global', 'country', 'geostore', 'aoi', 'wdpa', 'use'], + }, + ], settingsConfig: [ { key: 'years', @@ -36,12 +42,6 @@ export default { type: 'range-select', border: true, }, - { - key: 'threshold', - label: 'canopy density', - type: 'mini-select', - metaKey: 'widget_canopy_density', - }, ], refetchKeys: ['threshold'], chartType: 'composedChart', @@ -53,10 +53,11 @@ export default { layers: [DISPUTED_POLITICAL_BOUNDARIES, POLITICAL_BOUNDARIES], boundary: true, }, + // natural forest { - // global plantations - dataset: TREE_PLANTATIONS_DATASET, - layers: [TREE_PLANTATIONS], + dataset: NATURAL_FOREST, + layers: [NATURAL_FOREST_2020], + boundary: true, }, // loss { @@ -64,11 +65,12 @@ export default { layers: [FOREST_LOSS], }, ], + dataType: 'naturalForest', sortOrder: { forestChange: 2, }, sentence: - 'From {startYear} to {endYear}, {percentage} of tree cover loss in {location} occurred within {lossPhrase}. The total loss within natural forest was equivalent to {value} of CO\u2082e emissions.', + 'From {startYear} to {endYear}, {percentage} of tree cover loss in {location} occurred within {lossPhrase}. The total loss within natural forest was {totalLoss} equivalent to {value} of CO\u2082e emissions.', whitelists: { indicators: ['plantations'], checkStatus: true, @@ -80,23 +82,12 @@ export default { extentYear: 2010, }, getData: (params) => - all([ - getLoss({ ...params, forestType: 'plantations' }), - getLoss({ ...params, forestType: '' }), - ]).then( - spread((plantationsloss, gadmLoss) => { + all([getLossNaturalForest(params)]).then( + spread((gadmLoss) => { let data = {}; - const lossPlantations = - plantationsloss.data && plantationsloss.data.data; const totalLoss = gadmLoss.data && gadmLoss.data.data; - if ( - lossPlantations && - totalLoss && - lossPlantations.length && - totalLoss.length - ) { + if (totalLoss && totalLoss.length) { data = { - lossPlantations, totalLoss, }; } @@ -118,8 +109,10 @@ export default { }) ), getDataURL: (params) => [ - getLoss({ ...params, forestType: 'plantations', download: true }), - getLoss({ ...params, forestType: '', download: true }), + getLossNaturalForest({ + ...params, + download: true, + }), ], getWidgetProps, }; diff --git a/components/widgets/forest-change/tree-loss-plantations/selectors.js b/components/widgets/forest-change/tree-loss-plantations/selectors.js index bd1f4816e7..e7d2074747 100644 --- a/components/widgets/forest-change/tree-loss-plantations/selectors.js +++ b/components/widgets/forest-change/tree-loss-plantations/selectors.js @@ -1,13 +1,11 @@ import { createSelector, createStructuredSelector } from 'reselect'; import sumBy from 'lodash/sumBy'; -import groupBy from 'lodash/groupBy'; import uniqBy from 'lodash/uniqBy'; import { formatNumber } from 'utils/format'; import { getColorPalette } from 'components/widgets/utils/colors'; -import { zeroFillYears } from 'components/widgets/utils/data'; +import { zeroFillYearsFilter } from 'components/widgets/utils/data'; // get list data -const getLossPlantations = (state) => state.data && state.data.lossPlantations; const getTotalLoss = (state) => state.data && state.data.totalLoss; const getSettings = (state) => state.settings; const getLocationName = (state) => state.locationLabel; @@ -16,9 +14,9 @@ const getSentence = (state) => state.sentence; // get lists selected export const parseData = createSelector( - [getLossPlantations, getTotalLoss, getSettings], - (lossPlantations, totalLoss, settings) => { - if (!lossPlantations || !totalLoss) return null; + [getTotalLoss, getSettings], + (totalLoss, settings) => { + if (!totalLoss) return null; const { startYear, endYear, yearsRange } = settings; const years = yearsRange && yearsRange.map((yearObj) => yearObj.value); const fillObj = { @@ -28,42 +26,41 @@ export const parseData = createSelector( emissions: 0, percentage: 0, }; - const zeroFilledData = zeroFillYears( - lossPlantations, + const zeroFilledData = zeroFillYearsFilter( + totalLoss, startYear, endYear, years, fillObj ); - const totalLossByYear = groupBy(totalLoss, 'year'); - const parsedData = uniqBy( - zeroFilledData - .filter((d) => d.year >= startYear && d.year <= endYear) - .map((d) => { - const groupedPlantations = groupBy(lossPlantations, 'year')[d.year]; - const summedPlatationsLoss = - (groupedPlantations && sumBy(groupedPlantations, 'area')) || 0; - const summedPlatationsEmissions = - (groupedPlantations && sumBy(groupedPlantations, 'emissions')) || 0; - const totalLossForYear = - (totalLossByYear[d.year] && totalLossByYear[d.year][0]) || {}; - const returnData = { - ...d, - outsideAreaLoss: totalLossForYear.area - summedPlatationsLoss, - areaLoss: summedPlatationsLoss || 0, - totalLoss: totalLossForYear.area || 0, - outsideCo2Loss: - totalLossByYear[d.year]?.[0]?.emissions - - summedPlatationsEmissions, - co2Loss: summedPlatationsEmissions || 0, - }; - return returnData; - }), - 'year' - ); + const mappedData = zeroFilledData.map((list) => { + const naturalForest = list.find( + (item) => item.sbtn_natural_forests__class === 'Natural Forest' + ); + const nonNaturalForest = list.find( + (item) => item.sbtn_natural_forests__class === 'Non-Natural Forest' + ); + // eslint-disable-next-line no-unused-vars + const unknown = list.find( + (item) => item.sbtn_natural_forests__class === 'Unknown' + ); + + return { + iso: nonNaturalForest?.iso, + outsideAreaLoss: naturalForest?.area || 0, + outsideCo2Loss: naturalForest?.emissions || 0, + areaLoss: nonNaturalForest?.area || 0, + co2Loss: nonNaturalForest?.emissions || 0, + totalLoss: (nonNaturalForest?.area || 0) + (naturalForest?.area || 0), + year: nonNaturalForest?.year, + }; + }); + + const parsedData = uniqBy(mappedData, 'year'); + return parsedData; - } + }, ); export const parseConfig = createSelector([getColors], (colors) => { @@ -103,7 +100,7 @@ export const parseConfig = createSelector([getColors], (colors) => { }, { key: 'areaLoss', - label: 'Plantations', + label: 'Non-natural tree cover', color: colorRange[0], unitFormat: (value) => formatNumber({ num: value, unit: 'ha', spaceUnit: true }), @@ -117,17 +114,12 @@ export const parseSentence = createSelector( (data, settings, locationName, sentence) => { if (!data) return null; const { startYear, endYear } = settings; - const plantationsLoss = sumBy(data, 'areaLoss') || 0; const totalLoss = sumBy(data, 'totalLoss') || 0; const outsideLoss = sumBy(data, 'outsideAreaLoss') || 0; const outsideEmissions = sumBy(data, 'outsideCo2Loss') || 0; - const lossPhrase = - plantationsLoss > outsideLoss ? 'plantations' : 'natural forest'; - const percentage = - plantationsLoss > outsideLoss - ? (100 * plantationsLoss) / totalLoss - : (100 * outsideLoss) / totalLoss; + const lossPhrase = 'natural forest'; + const percentage = (100 * outsideLoss) / totalLoss; const params = { location: locationName, startYear, @@ -139,6 +131,7 @@ export const parseSentence = createSelector( spaceUnit: true, }), percentage: formatNumber({ num: percentage, unit: '%' }), + totalLoss: formatNumber({ num: outsideLoss, unit: 'ha' }), // using outsideLoss (natural forest) value based on Michelle's feedback }; return { diff --git a/components/widgets/land-cover/natural-forest/index.js b/components/widgets/land-cover/natural-forest/index.js new file mode 100644 index 0000000000..5a001976cb --- /dev/null +++ b/components/widgets/land-cover/natural-forest/index.js @@ -0,0 +1,117 @@ +import { getNaturalForest } from 'services/analysis-cached'; +import { NATURAL_FOREST, POLITICAL_BOUNDARIES_DATASET } from 'data/datasets'; +import { + NATURAL_FOREST_2020, + DISPUTED_POLITICAL_BOUNDARIES, + POLITICAL_BOUNDARIES, +} from 'data/layers'; + +import getWidgetProps from './selectors'; + +export default { + widget: 'naturalForest', + title: { + default: 'Natural forest in {location}', + global: 'Global natural forest', + }, + sentence: { + default: { + global: `As of 2020, {naturalForestPercentage} of global land cover was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`, + region: `As of 2020, {naturalForestPercentage} of land cover in {location} was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`, + }, + withIndicator: { + global: `As of 2020, {naturalForestPercentage} of global land cover in {indicator} was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`, + region: `As of 2020, {naturalForestPercentage} of land cover in {indicator} in {location} was natural forests and {nonNaturalForestPercentage} was non-natural tree cover.`, + }, + }, + metaKey: { + 2000: 'sbtn_natural_forests_map', + 2010: 'sbtn_natural_forests_map', + 2020: 'sbtn_natural_forests_map', + }, + chartType: 'pieChart', + large: false, + colors: 'extent', + source: 'gadm', + categories: ['land-cover', 'summary'], + types: ['global', 'country', 'geostore', 'aoi', 'wdpa', 'use'], + admins: ['global', 'adm0', 'adm1', 'adm2'], + visible: ['dashboard'], + datasets: [ + { + dataset: POLITICAL_BOUNDARIES_DATASET, + layers: [DISPUTED_POLITICAL_BOUNDARIES, POLITICAL_BOUNDARIES], + boundary: true, + }, + { + dataset: NATURAL_FOREST, + layers: [NATURAL_FOREST_2020], + }, + ], + dataType: 'naturalForest', + sortOrder: { + summary: 6, + landCover: 1, + }, + refetchKeys: ['threshold', 'decile', 'extentYear', 'landCategory'], + pendingKeys: ['threshold', 'decile', 'extentYear'], + settings: { + extentYear: 2000, + }, + getSettingsConfig: () => { + return [ + { + key: 'landCategory', + label: 'Land Category', + type: 'select', + placeholder: 'All categories', + clearable: true, + border: true, + }, + ]; + }, + getData: (params) => { + const { threshold, decile, ...filteredParams } = params; + + return getNaturalForest({ ...filteredParams }).then((response) => { + const extent = response.data; + + let totalNaturalForest = 0; + let totalNonNaturalTreeCover = 0; + let unknown = 0; + + let data = {}; + if (extent && extent.length) { + // Sum values + extent.forEach((item) => { + switch (item.sbtn_natural_forests__class) { + case 'Natural Forest': + totalNaturalForest += item.area__ha; + break; + case 'Non-Natural Forest': + totalNonNaturalTreeCover += item.area__ha; + break; + default: + // 'Unknown' + unknown += item.area__ha; + } + }); + + data = { + totalNaturalForest, + unknown, + totalNonNaturalTreeCover, + totalArea: totalNaturalForest + unknown + totalNonNaturalTreeCover, + }; + } + + return data; + }); + }, + getDataURL: async (params) => { + const response = await getNaturalForest({ ...params, download: true }); + + return [response]; + }, + getWidgetProps, +}; diff --git a/components/widgets/land-cover/natural-forest/selectors.js b/components/widgets/land-cover/natural-forest/selectors.js new file mode 100644 index 0000000000..a64ed8a599 --- /dev/null +++ b/components/widgets/land-cover/natural-forest/selectors.js @@ -0,0 +1,126 @@ +import { createSelector, createStructuredSelector } from 'reselect'; +import isEmpty from 'lodash/isEmpty'; +import { formatNumber } from 'utils/format'; + +const getData = (state) => state.data; +const getSettings = (state) => state.settings; +const getIndicator = (state) => state.indicator; +const getWhitelist = (state) => state.polynamesWhitelist; +const getSentence = (state) => state.sentence; +const getTitle = (state) => state.title; +const getLocationName = (state) => state.locationLabel; +const getMetaKey = (state) => state.metaKey; +const getAdminLevel = (state) => state.adminLevel; + +export const isoHasPlantations = createSelector( + [getWhitelist, getLocationName], + (whitelist, name) => { + const hasPlantations = + name === 'global' + ? true + : whitelist && + whitelist.annual && + whitelist.annual.includes('plantations'); + return hasPlantations; + } +); + +export const parseData = createSelector([getData], (data) => { + if (isEmpty(data)) { + return null; + } + + const { totalNaturalForest, unknown, totalNonNaturalTreeCover, totalArea } = + data; + const parsedData = [ + { + label: 'Natural forests', + value: totalNaturalForest, + color: '#2C6639', + percentage: (totalNaturalForest / totalArea) * 100, + }, + { + label: 'Non-natural tree cover', + value: totalNonNaturalTreeCover, + color: '#A8DDB5', + percentage: (totalNonNaturalTreeCover / totalArea) * 100, + }, + { + label: 'Other land cover', + value: unknown, + color: '#D3D3D3', + percentage: (unknown / totalArea) * 100, + }, + ]; + + return parsedData; +}); + +export const parseTitle = createSelector( + [getTitle, getLocationName], + (title, name) => { + return name === 'global' ? title.global : title.default; + } +); + +export const parseSentence = createSelector( + [ + getData, + getSettings, + getLocationName, + getIndicator, + getSentence, + getAdminLevel, + ], + (data, settings, locationName, indicator, sentences, admLevel) => { + if (!data || !sentences) return null; + + const { extentYear, threshold, decile } = settings; + + const isTropicalTreeCover = extentYear === 2020; + const decileThreshold = isTropicalTreeCover ? decile : threshold; + const withIndicator = !!indicator; + const sentenceKey = withIndicator ? 'withIndicator' : 'default'; + const sentenceSubkey = admLevel === 'global' ? 'global' : 'region'; + const sentence = sentences[sentenceKey][sentenceSubkey]; + + const { totalNaturalForest, totalNonNaturalTreeCover, totalArea } = data; + const percentNaturalForest = (100 * totalNaturalForest) / totalArea; + const percentNonNaturalForest = + (100 * totalNonNaturalTreeCover) / totalArea; + + const formattedNaturalForestPercentage = formatNumber({ + num: percentNaturalForest, + unit: '%', + }); + const formattedNonNaturalForestPercentage = formatNumber({ + num: percentNonNaturalForest, + unit: '%', + }); + + const thresholdLabel = `>${decileThreshold}%`; + + const params = { + year: extentYear, + location: locationName, + naturalForestPercentage: formattedNaturalForestPercentage, + nonNaturalForestPercentage: formattedNonNaturalForestPercentage, + indicator: indicator?.label, + threshold: thresholdLabel, + }; + + return { sentence, params }; + } +); + +export const parseMetaKey = createSelector( + [getMetaKey, getSettings], + (metaKey, settings) => metaKey[settings.extentYear] +); + +export default createStructuredSelector({ + data: parseData, + sentence: parseSentence, + title: parseTitle, + metaKey: parseMetaKey, +}); diff --git a/components/widgets/land-cover/tree-cover-ranked/index.js b/components/widgets/land-cover/tree-cover-ranked/index.js index abecb46cc8..3ac9b4bcaa 100644 --- a/components/widgets/land-cover/tree-cover-ranked/index.js +++ b/components/widgets/land-cover/tree-cover-ranked/index.js @@ -76,7 +76,7 @@ export default { ], sortOrder: { summary: 1, - landCover: 1, + landCover: 1.1, }, refetchKeys: ['threshold', 'extentYear', 'forestType', 'landCategory'], settings: { diff --git a/components/widgets/manifest.js b/components/widgets/manifest.js index fff967ba1b..d9c8a2f725 100644 --- a/components/widgets/manifest.js +++ b/components/widgets/manifest.js @@ -42,6 +42,7 @@ import treeCoverLocated from 'components/widgets/land-cover/tree-cover-located'; import USLandCover from 'components/widgets/land-cover/us-land-cover'; import rankedForestTypes from 'components/widgets/land-cover/ranked-forest-types'; import treeCoverDensity from 'components/widgets/land-cover/tree-cover-density'; +import naturalForest from 'components/widgets/land-cover/natural-forest'; // Climate import woodyBiomass from 'components/widgets/climate/whrc-biomass/'; @@ -103,6 +104,7 @@ export default { treeCoverLocated, rankedForestTypes, treeCoverDensity, + naturalForest, // climate // emissions, diff --git a/components/widgets/utils/config.js b/components/widgets/utils/config.js index bbd26ce5f4..6922ded118 100644 --- a/components/widgets/utils/config.js +++ b/components/widgets/utils/config.js @@ -440,6 +440,10 @@ export const getStatements = ({ ...(indicatorStatements || []), ]); + if (dataType === 'naturalForest') { + return []; + } + return statements; }; diff --git a/components/widgets/utils/data.js b/components/widgets/utils/data.js index bb01389880..9bc3de3f44 100644 --- a/components/widgets/utils/data.js +++ b/components/widgets/utils/data.js @@ -464,6 +464,28 @@ export const zeroFillYears = (data, startYear, endYear, years, fillObj) => { return zeroFilledData; }; +export const zeroFillYearsFilter = ( + data, + startYear, + endYear, + years, + fillObj +) => { + const zeroFilledData = []; + if (years) { + years + .filter((year) => year >= startYear && year <= endYear) + .forEach((year) => { + const yearData = data.filter((o) => o.year === year) || { + ...fillObj, + year, + }; + zeroFilledData.push(yearData); + }); + } + return zeroFilledData; +}; + export const getWeeksRange = (weeks) => { const endDate = moment().format('YYYY-MM-DD'); const startDate = moment(endDate) diff --git a/data/datasets.js b/data/datasets.js index fe66dbfe4f..7e7037c84a 100644 --- a/data/datasets.js +++ b/data/datasets.js @@ -55,3 +55,4 @@ export const PRIMARY_FOREST_DATASET = 'primary-forests'; export const MANGROVE_FORESTS_DATASET = 'mangrove-forests'; export const GFW_STORIES_DATASET = 'mongabay-stories'; export const TROPICAL_TREE_COVER_DATASET = 'tropical-tree-cover'; +export const NATURAL_FOREST = 'natural-forests'; diff --git a/data/layers.js b/data/layers.js index 06a3a33760..1ed343e8a7 100644 --- a/data/layers.js +++ b/data/layers.js @@ -60,3 +60,4 @@ export const PRIMARY_FOREST = 'primary-forests-2001'; export const MANGROVE_FORESTS = 'mangrove-forests-1996'; export const TROPICAL_TREE_COVER_HECTARE = 'tropical-tree-cover-hectare'; export const TROPICAL_TREE_COVER_METERS = 'tropical-tree-cover-meters'; +export const NATURAL_FOREST_2020 = 'natural-forests-2020'; diff --git a/services/analysis-cached.js b/services/analysis-cached.js index 81ea8a957a..ed5d30dbad 100644 --- a/services/analysis-cached.js +++ b/services/analysis-cached.js @@ -21,6 +21,7 @@ const SQL_QUERIES = { lossTsc: 'SELECT tsc_tree_cover_loss_drivers__driver, umd_tree_cover_loss__year, SUM(umd_tree_cover_loss__ha) AS umd_tree_cover_loss__ha, SUM("gfw_gross_emissions_co2e_all_gases__Mg") AS "gfw_gross_emissions_co2e_all_gases__Mg" FROM data {WHERE} GROUP BY tsc_tree_cover_loss_drivers__driver, umd_tree_cover_loss__year', loss: 'SELECT {select_location}, umd_tree_cover_loss__year, SUM(umd_tree_cover_loss__ha) AS umd_tree_cover_loss__ha, SUM("gfw_gross_emissions_co2e_all_gases__Mg") AS "gfw_gross_emissions_co2e_all_gases__Mg" FROM data {WHERE} GROUP BY umd_tree_cover_loss__year, {location} ORDER BY umd_tree_cover_loss__year, {location}', + lossNaturalForest: `SELECT {select_location}, sbtn_natural_forests__class, umd_tree_cover_loss__year, SUM(umd_tree_cover_loss__ha) AS umd_tree_cover_loss__ha, SUM("gfw_gross_emissions_co2e_all_gases__Mg") AS gfw_gross_emissions_co2e_all_gases__Mg FROM data {WHERE} GROUP BY sbtn_natural_forests__class, umd_tree_cover_loss__year, {location}`, lossFires: 'SELECT {select_location}, umd_tree_cover_loss__year, SUM(umd_tree_cover_loss__ha) AS umd_tree_cover_loss__ha, SUM(umd_tree_cover_loss_from_fires__ha) AS "umd_tree_cover_loss_from_fires__ha" FROM data {WHERE} GROUP BY umd_tree_cover_loss__year, {location} ORDER BY umd_tree_cover_loss__year, {location}', lossFiresOTF: @@ -36,6 +37,7 @@ const SQL_QUERIES = { carbonFluxOTF: `SELECT SUM("gfw_forest_carbon_net_flux__Mg_CO2e"), SUM("gfw_forest_carbon_gross_removals__Mg_CO2e"), SUM("gfw_forest_carbon_gross_emissions__Mg_CO2e") FROM data WHERE umd_tree_cover_density_2000__threshold >= {threshold} OR is__umd_tree_cover_gain = 'true'&geostore_origin={geostoreOrigin}&geostore_id={geostoreId}`, extent: 'SELECT {select_location}, SUM(umd_tree_cover_extent_{extentYear}__ha) AS umd_tree_cover_extent_{extentYear}__ha, SUM(area__ha) AS area__ha FROM data {WHERE} GROUP BY {location} ORDER BY {location}', + extentNaturalForest: `SELECT {select_location}, sbtn_natural_forests__class, SUM(area__ha) AS area__ha FROM data {WHERE} GROUP BY iso, sbtn_natural_forests__class, {location} ORDER BY {location}`, gain: `SELECT {select_location}, SUM("umd_tree_cover_gain__ha") AS "umd_tree_cover_gain__ha", SUM(umd_tree_cover_extent_2000__ha) AS umd_tree_cover_extent_2000__ha FROM data {WHERE} AND umd_tree_cover_gain__period in ({baselineYear}) GROUP BY {location} ORDER BY {location}`, areaIntersection: 'SELECT {select_location}, SUM(area__ha) AS area__ha {intersection} FROM data {WHERE} GROUP BY {location} {intersection} ORDER BY area__ha DESC', @@ -83,6 +85,8 @@ const SQL_QUERIES = { treeCoverOTFExtent: 'SELECT SUM(area__ha) FROM data&geostore_id={geostoreId}', treeCoverGainSimpleOTF: 'SELECT SUM(area__ha) FROM data&geostore_id={geostoreId}', + naturalForest: + 'SELECT {location}, sbtn_natural_forests__class, SUM(area__ha) AS area__ha FROM data {WHERE} GROUP BY {location}, sbtn_natural_forests__class', netChangeIso: 'SELECT {select_location}, stable, loss, gain, disturb, net, change, gfw_area__ha FROM data {WHERE}', netChange: @@ -378,6 +382,56 @@ export const getTreeCoverLossByDriverType = (params) => { })); }; +export const getLossNaturalForest = (params) => { + const { forestType, landCategory, ifl, download } = params || {}; + + const requestUrl = getRequestUrl({ + ...params, + dataset: 'annual', + datasetType: 'change', + version: 'v20240815', + }); + + if (!requestUrl) { + return new Promise(() => {}); + } + + const url = encodeURI( + `${requestUrl}${SQL_QUERIES.lossNaturalForest}` + .replace( + /{select_location}/g, + getLocationSelect({ ...params, cast: false }) + ) + .replace(/{location}/g, getLocationSelect(params)) + .replace( + '{WHERE}', + getWHEREQuery({ ...params, dataset: 'annual', threshold: 0 }) + ) + ); + + if (download) { + const indicator = getIndicator(forestType, landCategory, ifl); + return { + name: `loss_natural_forest${ + indicator ? `_in_${snakeCase(indicator.label)}` : '' + }__ha`, + url: getDownloadUrl(url), + }; + } + + return dataRequest.get(url).then((response) => ({ + ...response, + data: { + data: response?.data?.map((d) => ({ + ...d, + year: d.umd_tree_cover_loss__year, + area: d.umd_tree_cover_loss__ha, + emissions: d.gfw_gross_emissions_co2e_all_gases__mg, + })), + }, + })); +}; + // summed loss for single location export const getLoss = (params) => { const { forestType, landCategory, ifl, download } = params || {}; @@ -1041,6 +1095,40 @@ export const getTropicalExtentGrouped = (params) => { })); }; +export const getNaturalForest = async (params) => { + const { download } = params || {}; + + const requestUrl = getRequestUrl({ + ...params, + dataset: 'annual', + datasetType: 'summary', + version: 'v20240815', + }); + + if (!requestUrl) { + return new Promise(() => {}); + } + + const url = encodeURI( + `${requestUrl}${SQL_QUERIES.naturalForest}` + .replace(/{location}/g, getLocationSelect({ ...params, cast: false })) + .replace(/{location}/g, getLocationSelect({ ...params })) + .replace( + '{WHERE}', + getWHEREQuery({ ...params, dataset: 'annual', threshold: 0 }) + ) + ); + + if (download) { + return { + name: `natural_forest_2020__ha`, + url: getDownloadUrl(url), + }; + } + + return dataRequest.get(url); +}; + export const getTreeCoverByLandCoverClass = (params) => { const { forestType, download, extentYear, landCategory, ifl } = params || {}; @@ -1124,6 +1212,62 @@ export const getNetChange = async (params) => { }; }; +export const getExtentNaturalForest = (params) => { + const { forestType, landCategory, ifl, download } = params || {}; + + const requestUrl = getRequestUrl({ + ...params, + dataset: 'annual', + datasetType: 'summary', + version: 'v20240815', + }); + + if (!requestUrl) { + return new Promise(() => {}); + } + + const url = encodeURI( + `${requestUrl}${SQL_QUERIES.extentNaturalForest}` + .replace( + /{select_location}/g, + getLocationSelect({ ...params, cast: false }) + ) + .replace(/{location}/g, getLocationSelect({ ...params })) + .replace( + '{WHERE}', + getWHEREQuery({ ...params, dataset: 'annual', threshold: 0 }) + ) + ); + + if (download) { + const indicator = getIndicator(forestType, landCategory, ifl); + return { + name: `natural_forest_${ + indicator ? `_in_${snakeCase(indicator.label)}` : '' + }__ha`, + url: getDownloadUrl(url), + }; + } + + return dataRequest.get(url).then((response) => { + return { + ...response, + data: { + data: response?.data?.map((d) => { + return { + ...d, + extent: + d.sbtn_natural_forests__class === 'Natural Forest' + ? d.area__ha + : 0, + total_area: d.area__ha, + }; + }), + }, + }; + }); +}; + // summed extent for single location export const getExtent = (params) => { const { forestType, landCategory, ifl, download, extentYear } = params || {}; diff --git a/services/get-where-query.js b/services/get-where-query.js index b00d854c68..d95df2cc2f 100644 --- a/services/get-where-query.js +++ b/services/get-where-query.js @@ -8,7 +8,9 @@ const isNumber = (value) => !!(typeof value === 'number' || !isNaN(value)); // build {where} statement for query export const getWHEREQuery = (params = {}) => { - const { type, dataset } = params || {}; + // umd_tree_cover_loss__year is being added for the dashboard sentences for natural forest + const { type, dataset, umd_tree_cover_loss__year, isNaturalForest } = + params || {}; const allFilterOptions = forestTypes.concat(landCategories); const allowedParams = ALLOWED_PARAMS[params.dataset || 'annual']; @@ -98,7 +100,11 @@ export const getWHEREQuery = (params = {}) => { } if (isLastParameter) { - WHERE = `${WHERE} `; + if (isNaturalForest) { + WHERE = `${WHERE} AND umd_tree_cover_loss__year=${umd_tree_cover_loss__year}`; + } else { + WHERE = `${WHERE} `; + } } else { WHERE = `${WHERE} AND `; } diff --git a/services/sentences.js b/services/sentences.js index 61461470c6..68484d5d26 100644 --- a/services/sentences.js +++ b/services/sentences.js @@ -8,9 +8,12 @@ import max from 'lodash/max'; import reverse from 'lodash/reverse'; import isEmpty from 'lodash/isEmpty'; -import tropicalIsos from 'data/tropical-isos.json'; - -import { getExtent, getLoss } from 'services/analysis-cached'; +import { + getExtentNaturalForest, + getLossNaturalForest, + getExtent, + getLoss, +} from 'services/analysis-cached'; const ADMINS = { adm0: null, @@ -27,13 +30,13 @@ const GLOBAL_LOCATION = { export const adminSentences = { default: - 'In 2010, {location} had {extent} of tree cover, extending over {percentage} of its land area.', + 'In 2020, {location} had {extent} of natural forest, extending over {percentage} of its land area', withLoss: - 'In 2010, {location} had {extent} of tree cover, extending over {percentage} of its land area. In {year}, it lost {loss} of tree cover', + 'In 2020, {location} had {extent} of natural forest, extending over {percentage} of its land area. In {year}, it lost {loss} of natural forest', globalInitial: - 'In 2010, {location} had {extent} of tree cover, extending over {percentage} of its land area. In {year}, it lost {loss} of tree cover.', + 'In 2020, {location} had {extent} of natural forest, extending over {percentage} of its land area. In {year}, it lost {loss} of natural forest', withPlantationLoss: - 'In 2010, {location} had {naturalForest} of natural forest, extending over {percentage} of its land area. In {year}, it lost {naturalLoss} of natural forest', + 'In 2020, {location} had {naturalForest} of natural forest, extending over {percentage} of its land area. In {year}, it lost {naturalLoss} of natural forest', countrySpecific: { IDN: 'In 2001, {location} had {primaryForest} of primary forest*, extending over {percentagePrimaryForest} of its land area. In {year}, it lost {primaryLoss} of primary forest*, equivalent to {emissionsPrimary} of CO₂ emissions.', }, @@ -41,7 +44,7 @@ export const adminSentences = { end: '.', }; -export const getSentenceData = (params = GLOBAL_LOCATION) => +const getSentenceDataForIdn = (params = GLOBAL_LOCATION) => all([ getExtent(params), getExtent({ ...params, forestType: 'plantations' }), @@ -141,6 +144,80 @@ export const getSentenceData = (params = GLOBAL_LOCATION) => ) ); +const getNaturalForestSentenceData = async (params = GLOBAL_LOCATION) => { + try { + const extentNaturalForestResponse = await getExtentNaturalForest(params); + const lossNaturalForestResponse = await getLossNaturalForest({ + ...params, + umd_tree_cover_loss__year: 2023, + isNaturalForest: true, + }); + + let extent = 0; + let totalArea = 0; + + extentNaturalForestResponse.data.data.forEach((item) => { + totalArea += item.area__ha; + + if (item.sbtn_natural_forests__class === 'Natural Forest') + extent += item.area__ha; + }); + + let lossArea = 0; + let emissions = 0; + + lossNaturalForestResponse.data.data.forEach((item) => { + emissions += item.gfw_gross_emissions_co2e_all_gases__mg; + + if (item.sbtn_natural_forests__class === 'Natural Forest') { + lossArea += item.area; + } + }); + + return { + totalArea, + extent, + plantationsExtent: 0, + primaryExtent: 0, + totalLoss: { + area: lossArea, + year: 2023, + emissions, + }, + plantationsLoss: { + area: 0, + emissions: 0, + }, + primaryLoss: {}, + }; + } catch (error) { + return { + totalArea: 0, + extent: 0, + plantationsExtent: 0, + primaryExtent: 0, + totalLoss: { + area: 0, + year: 0, + emissions: 0, + }, + plantationsLoss: { + area: 0, + emissions: 0, + }, + primaryLoss: {}, + }; + } +}; + +export const getSentenceData = async (params = GLOBAL_LOCATION) => { + if (params.adm0 === 'IDN') { + return getSentenceDataForIdn(params); + } + + return getNaturalForestSentenceData(params); +}; + export const getContextSentence = (location, geodescriber, adminSentence) => { if (isEmpty(geodescriber)) return {}; @@ -174,7 +251,6 @@ export const parseSentence = ( globalInitial, countrySpecific, co2Emissions, - end, } = adminSentences; const { extent, @@ -283,14 +359,16 @@ export const parseSentence = ( if (extent > 0 && totalLoss.area) { sentence = areaPlantations && location ? withPlantationLoss : withLoss; } - sentence = tropicalIsos.includes(adm0) - ? sentence + co2Emissions - : sentence + end; + if (!location) sentence = globalInitial; if (adm0 in countrySpecific) { sentence = countrySpecific[adm0]; } + if (adm0 !== 'IDN') { + sentence += co2Emissions; + } + return { sentence, params,