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

3203 Forest area as proportion of total land area SDG - should be estimated bases on the latest available NDP after 2020 #3427

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ExpressionFunction } from '@openforis/arena-core/dist/expression/function'
import { Objects } from 'utils/objects'

import { Context } from 'meta/expressionEvaluator/context'

type Year = number
type Data = Record<Year, Record<'extentOfForest' | 'totalLandArea', number>>

const minYear = 2020
const maxYear = 2025

// ---------- Helper utility functions

// Helper utility to create a data object
const _getData = (
forestAreaData: Array<string> | undefined,
totalLandAreaData: Array<string> | undefined
): { data: Data } => {
const data = [0, 1, 2, 3, 4, 5].reduce<Data>((acc, i) => {
const key = minYear + i
// eslint-disable-next-line no-param-reassign
acc[key] = {
extentOfForest: parseFloat(forestAreaData[i]),
totalLandArea: parseFloat(totalLandAreaData[i]),
}

return acc
}, {} as Data)

return { data }
}

// Helper utility to get the range of years
const _getRange = (data: Data, year: Year): { left: Year; right: Year } => {
const years = Object.keys(data).reduce<Array<Year>>((acc, y) => {
if (!Objects.isEmpty(data[Number(y)].extentOfForest)) acc.push(parseInt(y, 10))
return acc
}, [])

const left = [...years].reverse().find((y) => y < year) || minYear
const right = years.find((y) => y > year) || maxYear

return { left, right }
}

const _getProportion = (a: number, b: number): number => {
return (a / b) * 100
}

const _getYearProportion = (data: Data, year: Year): number => {
const { extentOfForest, totalLandArea } = data[year]
return _getProportion(extentOfForest, totalLandArea)
}

/**
* @name calculatorForestAreaAsProportionOfTotalLandArea
* @description
* Calculates the forest area as a proportion of the total land area.
* Primarily used for Section 8 Table SDG 15.1.1.
*
* @param {string} year - The year of the calculation.
* @param {Array<string>} valuesExtentOfForest - The values of the extent of forest subcategories for years 2020...2025
* @param {Array<string>} valuesTotalLandArea - The values of the total land area subcategories for years 2020...2025
*/
export const calculatorForestAreaAsProportionOfTotalLandArea: ExpressionFunction<Context> = {
name: 'calculatorForestAreaAsProportionOfTotalLandArea',
minArity: 2,
executor: () => {
return (
year: Year | undefined,
forestAreaData: Array<string> | undefined,
totalLandAreaData: Array<string> | undefined
// TODO: Arena-core/JSEP Doesn't support object format, see issue #3426
// data: Record<Year, Record<'extentOfForest' | 'totalLandArea', string>> | undefined
): number => {
if (!year || !forestAreaData?.length || !totalLandAreaData?.length) return null

const { data } = _getData(forestAreaData, totalLandAreaData)

// if we have value for current year
if (data[year].extentOfForest && data[year].totalLandArea) {
const { extentOfForest, totalLandArea } = data[year]
return _getProportion(extentOfForest, totalLandArea)
}

const { left, right } = _getRange(data, year)

const proportionLeft = _getYearProportion(data, left)
const proportionRight = _getYearProportion(data, right)

const proportion = proportionLeft + ((proportionRight - proportionLeft) * (year - left)) / (right - left)

return proportion
}
},
}
3 changes: 3 additions & 0 deletions src/meta/expressionEvaluator/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ExpressionFunction } from '@openforis/arena-core/dist/expression/function'

import { Context } from '../context'
import { calculatorForestAreaAsProportionOfTotalLandArea } from './calculations/calculatorForestAreaAsProportionOfTotalLandArea'
import { validatorSumSubCategoriesNotEqualToParent } from './subcategories/validatorSumSubCategoriesNotEqualToParent'
import { validatorSumSubCategoriesNotGreaterThanParent } from './subcategories/validatorSumSubCategoriesNotGreaterThanParent'
import { equalsWithTolerance } from './equalsWithTolerance'
Expand Down Expand Up @@ -42,6 +43,8 @@ import { validatorSumNotGreaterThanForest } from './validatorSumNotGreaterThanFo
import { validatorTotalForest } from './validatorTotalForest'

export const functions: Array<ExpressionFunction<Context>> = [
calculatorForestAreaAsProportionOfTotalLandArea,

NWFPProductHasCategory,
equalsWithTolerance,
maxForestArea,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as pgPromise from 'pg-promise'
import { Objects } from 'utils/objects'

import { Col } from 'meta/assessment'
import { NodeUpdate } from 'meta/data'

import { AssessmentController } from 'server/controller/assessment'
import { BaseProtocol, Schemas } from 'server/db'

import { updateDependencies } from 'test/migrations/steps/utils/updateDependencies'

const _years = [2020, 2021, 2022, 2023, 2024, 2025]
const _getCalcFormula = (year: string) => {
const eofString = _years.map((y) => `extentOfForest.forestArea['${y}']`).join(', ')
const tlaString = _years.map((y) => `extentOfForest.totalLandArea['${y}']`).join(', ')

return `calculatorForestAreaAsProportionOfTotalLandArea(${year}, [${eofString}], [${tlaString}])`
}

export default async (client: BaseProtocol) => {
const { assessment, cycle } = await AssessmentController.getOneWithCycle(
{ assessmentName: 'fra', cycleName: '2025', metaCache: true },
client
)

const schemaName = Schemas.getName(assessment)

const nodeMetadata = await client.map(
`
select c.*
from ${schemaName}.table t
left join ${schemaName}.row r on t.id = r.table_id
left join ${schemaName}.col c on r.id = c.row_id
where
t.props ->> 'name' = 'sustainableDevelopment15_1_1'
and r.props ->> 'variableName' = 'forestAreaProportionLandArea2015'
and c.props ->> 'colName' in ('2020', '2021', '2022', '2023', '2024')
`,
[],
(column) => {
// eslint-disable-next-line no-param-reassign
column.props.calculateFn[cycle.uuid] = _getCalcFormula(column.props.colName)
return column
}
)

const pgp = pgPromise()
const cs = new pgp.helpers.ColumnSet<Col>(
[
{
name: 'props',
cast: 'jsonb',
},
{
name: 'id',
cast: 'bigint',
cnd: true,
},
],
{
table: { table: 'col', schema: schemaName },
}
)

const query = `${pgp.helpers.update(nodeMetadata, cs)} WHERE v.id = t.id;`
await client.query(query)

// **** update metacache
await AssessmentController.generateMetaCache(client)

// **** update metadata cache
await AssessmentController.generateMetadataCache({ assessment }, client)

const update = await AssessmentController.getOneWithCycle(
{ assessmentName: 'fra', cycleName: '2025', metaCache: true },
client
)

// **** update calculated cols
const nodes = await client.map<NodeUpdate>(
`select s.props ->> 'name' as section_name
, t.props ->> 'name' as table_name
, r.props ->> 'variableName' as variable_name
, c.props ->> 'colName' as col_name
from ${schemaName}.col c
left join ${schemaName}.row r on r.id = c.row_id
left join ${schemaName}."table" t on t.id = r.table_id
left join ${schemaName}.table_section ts on ts.id = t.table_section_id
left join ${schemaName}.section s on s.id = ts.section_id
where s.props ->> 'name' = 'sustainableDevelopment'
and t.props ->> 'name' = 'sustainableDevelopment15_1_1'
and r.props ->> 'variableName' = 'forestAreaProportionLandArea2015'
and c.props ->> 'colName' in ('2020', '2021', '2022', '2023', '2024')`,
[],
(res) => Objects.camelize(res)
)

await updateDependencies(
{ assessment: update.assessment, cycle: update.cycle, nodes, includeSourceNodes: true },
client
)
}
Loading