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..dcf21e60f7 --- /dev/null +++ b/components/widgets/land-cover/natural-forest/index.js @@ -0,0 +1,111 @@ +import { getNaturalForest } from 'services/analysis-cached'; +import { NATURAL_FOREST } from 'data/datasets'; +import { NATURAL_FOREST_2020 } 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: NATURAL_FOREST, + layers: [NATURAL_FOREST_2020], + boundary: true, + }, + ], + dataType: 'naturalForest', + sortOrder: { + summary: 6, + landCover: 1, + }, + refetchKeys: ['threshold', 'decile', 'extentYear', 'landCategory'], + pendingKeys: ['threshold', 'decile', 'extentYear'], + settings: { + threshold: 30, + decile: 30, + 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/land-cover/tree-cover/index.js b/components/widgets/land-cover/tree-cover/index.js index ad5415a613..5adb358259 100644 --- a/components/widgets/land-cover/tree-cover/index.js +++ b/components/widgets/land-cover/tree-cover/index.js @@ -107,7 +107,7 @@ export default { ], sortOrder: { summary: 4, - landCover: 1, + landCover: 1.5, }, refetchKeys: ['threshold', 'decile', 'extentYear', 'landCategory'], pendingKeys: ['threshold', 'decile', 'extentYear'], diff --git a/components/widgets/manifest.js b/components/widgets/manifest.js index 7f257d94de..fe8b8c377b 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 9481fd6a56..9d5d4ea3b3 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/data/datasets.js b/data/datasets.js index ca9eea2d3d..a6db0e9fe5 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 28b73186fb..57a45c8811 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 39d76fc1f4..ad6c353114 100644 --- a/services/analysis-cached.js +++ b/services/analysis-cached.js @@ -81,6 +81,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: @@ -1022,6 +1024,37 @@ 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' })) + ); + + 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 || {};