Skip to content

Commit

Permalink
3203 Forest area as proportion of total land area SDG - should be est…
Browse files Browse the repository at this point in the history
…imated bases on the latest available NDP after 2020 (#3427)

* Initial commit for 3203

* Fix migration step

* use left/right for proportion

* deepscan

* Fix update deps
  • Loading branch information
sorja authored Mar 8, 2024
1 parent eb15829 commit c6658b4
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 0 deletions.
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
)
}

0 comments on commit c6658b4

Please sign in to comment.