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,