diff --git a/backend/Makefile b/backend/Makefile index fe499476..faa8336e 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -2,7 +2,7 @@ catalog: # All 4 Trees - Inventaire forestier coordo add kobotoolbox data/all4trees/inventaire_for/20260422_InventaireForestier_DonneesK.xlsx --form data/all4trees/inventaire_for/20260519_InventaireForestier_QuestionnaireK.xlsx --package catalog/inventaire_for - coordo add file data/all4trees/inventaire_for/20260703_InventaireForestier_DonneesExternes.xlsx --package catalog/inventaire_for + coordo add file data/all4trees/inventaire_for/20260713_InventaireForestier_DonneesExternes.xlsx --package catalog/inventaire_for coordo add foreignkey adu.decay for_dw.decay --package catalog/inventaire_for coordo add foreignkey inv_for.proj for_samp.proj --package catalog/inventaire_for @@ -27,12 +27,11 @@ catalog: coordo add foreignkey barbd_001.barbD_tax3 for_mf_tax3.tax3 --package catalog/inventaire_for # All 4 Trees - Inventaire Biologique - coordo add kobotoolbox data/all4trees/inventaire_bio/20260422_InventaireBiologique_DonneesK.xlsx --form data/all4trees/inventaire_bio/20260519_InventaireBiologique_QuestionnaireK.xlsx --package catalog/inventaire_bio - coordo add file data/all4trees/inventaire_bio/20260422_InventaireBiologique_DonneesExternes.xlsx --package catalog/inventaire_bio + coordo add kobotoolbox data/all4trees/inventaire_bio/20260710_InventaireBiologique_DonneesK.xlsx --form data/all4trees/inventaire_bio/20260709_InventaireBiologique_QuestionnaireK.xlsx --package catalog/inventaire_bio + coordo add file data/all4trees/inventaire_bio/20260713_InventaireBiologique_DonneesExternes.xlsx --package catalog/inventaire_bio coordo add foreignkey inv_bio.proj bio_samp.proj --package catalog/inventaire_bio coordo add foreignkey inv_bio.year bio_pop.year --package catalog/inventaire_bio - coordo add foreignkey inv_001.tax3 bio_sp.tax3 --package catalog/inventaire_bio # All 4 Trees - Enquête ménage coordo add kobotoolbox data/all4trees/enquete/20260422_EnqueteMenage_DonneesK.csv --form data/all4trees/enquete/20260519_EnqueteMenage_QuestionnaireK.xlsx --package catalog/enquete diff --git a/backend/configs/all4trees_config.json b/backend/configs/all4trees_config.json index 77be4689..035acbab 100644 --- a/backend/configs/all4trees_config.json +++ b/backend/configs/all4trees_config.json @@ -41,10 +41,10 @@ "resource": "inv_for", "layerType": "symbol", "cluster": { - "radius": 50, - "maxZoom": 12.5, + "radius": 40, + "maxZoom": 12, "steps": [10, 20], - "colors": ["#99D6C2", "#009966", "#006B47"] + "colors": ["rgba(153,214,194,0.7)", "rgba(0,153,102,0.7)", "rgba(0,107,71,0.7)"] }, "columns": { "id": "_id", @@ -91,6 +91,41 @@ "trigger": "click" } }, + { + "id": "inventaire_bio", + "type": "datapackage", + "path": "../catalog/inventaire_bio", + "resource": "inv_bio", + "layerType": "symbol", + "cluster": { + "radius": 40, + "maxZoom": 12, + "steps": [10, 20], + "colors": ["rgba(249,128,56,0.7)", "rgba(199,102,44,0.7)", "rgba(149,76,33,0.7)"] + }, + "columns": { + "geom": "start_point.merge().centroid()", + "id": "_id", + "conf": "conf", + "proj": "proj", + "ecos": "ecos", + "region": "loc1", + "forest": "loc2", + "start_date": "coh", + "taxon": "tax", + "type": "meth", + "samp_area": "inv_samp", + "samp_unit": "inv_unit", + "density": "int(sum(inv_001.no)) / inv_samp", + "dens_unit": "dens_unit", + "richness": "inv_001.count(no)", + "taxons_abundance_pop": "list_concat(list(inv_001.concat_ws(':', concat_ws('-',tax1, tax2, tax3), no) if inv_001.no > 0))", + "taxons_total_pop": "int(sum(inv_001.no))" + }, + "popup": { + "trigger": "click" + } + }, { "id": "enquete", "type": "datapackage", @@ -100,9 +135,9 @@ "groupby": ["loc2"], "cluster": { "radius": 50, - "maxZoom": 12.5, + "maxZoom": 12, "steps": [10, 20], - "colors": ["#A1B5F5", "#1447E6", "#0E32A1"] + "colors": ["rgba(161,181,245,0.7)", "rgba(20,71,230,0.7)", "rgba(14,50,161,0.7)"] }, "columns": { "geom": "centroid(merge(point(enq_gps.long, enq_gps.latit)))", diff --git a/backend/maps/views.py b/backend/maps/views.py index e7e53617..d0a4bfe2 100644 --- a/backend/maps/views.py +++ b/backend/maps/views.py @@ -13,7 +13,7 @@ from . import stats from .datapackage_manager import DatapackageManager -ALL4TREES_LAYERS = ['inventaire_for', 'enquete'] +ALL4TREES_LAYERS = ['inventaire_for', 'enquete', 'inventaire_bio'] config_path = settings.BASE_DIR / "configs" / "all4trees_config.json" map = Map.from_file(config_path) @@ -108,6 +108,7 @@ def remove_foreign_key_view(request): def get_map(user): user_map = copy(map) + filter = '' if user.is_authenticated: project = user.project if (project.lower() != ADMIN_PROJECT): diff --git a/backend/requirements.txt b/backend/requirements.txt index 68fbf144..04617858 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.8.1 chardet==7.4.3 -coordo @ git+https://github.com/dataforgoodfr/Coordonnees.git@0.8.0#subdirectory=coordo-py +coordo @ git+https://github.com/dataforgoodfr/Coordonnees.git@0.8.1#subdirectory=coordo-py Django>=4.2.27 djangorestframework==3.16.0 djangorestframework_simplejwt==5.5.1 diff --git a/webapp/biome.json b/webapp/biome.json index ea96cdd8..319ff7d3 100644 --- a/webapp/biome.json +++ b/webapp/biome.json @@ -3,7 +3,7 @@ "assist": { "actions": { "source": { - "organizeImports": { +"organizeImports": { "level": "on", "options": { "groups": [ @@ -16,6 +16,7 @@ "!@pages/**", "!@widgets/**", "!@features/**", + "!@entities/**", "!@shared/**", "!@lib/**", "!@i18n", @@ -31,6 +32,8 @@ ":BLANK_LINE:", "@features/**", ":BLANK_LINE:", + "@entities/**", + ":BLANK_LINE:", "@shared/**", "@lib/**", "@i18n", diff --git a/webapp/src/app/providers/map-provider-all4trees.tsx b/webapp/src/app/providers/map-provider-all4trees.tsx index 51771624..b28f1db2 100644 --- a/webapp/src/app/providers/map-provider-all4trees.tsx +++ b/webapp/src/app/providers/map-provider-all4trees.tsx @@ -5,10 +5,7 @@ import { useCategoriesFilters } from "@features/categories-filters/use-categorie import { renderAnchor, renderLayerRow } from "@features/controls/layer-control"; import { API_URL } from "@shared/api/client"; -import { - type Category, - MapContext, -} from "@shared/contexts/map-context-all4trees"; +import { MapContext } from "@shared/contexts/map-context-all4trees"; import { useLocalStorage } from "@shared/hooks/use-local-storage"; import { createMap, EVENTS } from "@shared/lib/coordo"; @@ -33,7 +30,6 @@ export function MapProviderAll4Trees({ children }: MapProviderAll4TreesProps) { const { isAuthenticated, token } = useAuth(); const mapApiRef = useRef | null>(null); - const [forests, setForests] = useState([]); const [mapSettings, setMapSettings] = useLocalStorage( "d4g:map-settings:all4trees", DEFAULT_MAP_SETTINGS, @@ -54,14 +50,6 @@ export function MapProviderAll4Trees({ children }: MapProviderAll4TreesProps) { const handleReady = () => { setIsReady(true); - // biome-ignore lint/suspicious/noExplicitAny : - const metadata: any = mapApiRef.current?.getLayerMetadata("inventaire"); - const forestField = metadata?.schema?.fields?.find( - (f: { name: string }) => f.name === "for", - ); - if (forestField?.categories) { - setForests(forestField.categories); - } // On first mount, sync the map state with the local storage state of "categories-filters" syncInitialCategoriesFilters({ @@ -104,12 +92,10 @@ export function MapProviderAll4Trees({ children }: MapProviderAll4TreesProps) { diff --git a/webapp/src/app/styles/all4trees.css b/webapp/src/app/styles/all4trees.css index 993e44a2..cf57a531 100644 --- a/webapp/src/app/styles/all4trees.css +++ b/webapp/src/app/styles/all4trees.css @@ -101,7 +101,7 @@ --chart-3: var(--a4t-color-citrouille); --chart-4: var(--a4t-color-vert-de-gris); --chart-5: #895bf5; - --chart-6: var(--a4t-color-onyx); + --chart-6: #da4238f3; --sidebar: var(--a4t-color-alabaster); --sidebar-foreground: var(--a4t-color-alabaster); diff --git a/webapp/src/entities/data.ts b/webapp/src/entities/data.ts new file mode 100644 index 00000000..157d78b4 --- /dev/null +++ b/webapp/src/entities/data.ts @@ -0,0 +1,25 @@ +export type ExternalData = { + for_label: LabelData[]; + bio_label: LabelData[]; + bio_sp: BioSpeciesData[]; + for_mf_tax1: any[]; + for_mf_tax2: any[]; + for_mf_tax3: any[]; + // Index signature pour accepter d'autres clés dynamiques si besoin + [key: string]: any[]; +}; + +export type LabelData = { + proj: string; + list_name: string; + name: number; + "label::fr": string; + "label::en": string; +}; + +export type BioSpeciesData = { + proj: string; + tax3: number; + "stat::fr": string; + "stat::en": string; +}; diff --git a/webapp/src/entities/resources.ts b/webapp/src/entities/resources.ts new file mode 100644 index 00000000..cae9ca9a --- /dev/null +++ b/webapp/src/entities/resources.ts @@ -0,0 +1,16 @@ +import { LAYERS } from "@shared/api/layers"; + +export const EXTERNAL_RESOURCES_BY_LAYER = new Map([ + [ + LAYERS.INVENTORY_FOR, + ["for_label", "for_mf_tax1", "for_mf_tax2", "for_mf_tax3", "for_score"], + ], + [LAYERS.INVENTORY_BIO, ["bio_label", "bio_sp"]], + [LAYERS.ENQUETE, [""]], + [LAYERS.SEED_POINT, [""]], +]); + +export const LABEL_DATA = new Map([ + [LAYERS.INVENTORY_FOR, "for_label"], + [LAYERS.INVENTORY_BIO, "bio_label"], +]); diff --git a/webapp/src/features/categories-filters/use-categories-config.tsx b/webapp/src/features/categories-filters/use-categories-config.tsx index 672b92b8..c078d653 100644 --- a/webapp/src/features/categories-filters/use-categories-config.tsx +++ b/webapp/src/features/categories-filters/use-categories-config.tsx @@ -20,15 +20,15 @@ export const useCategoriesConfig = (): { // ----- Actions ----- - const actionInventary: CategoryGroupItem = { + const actionInventory: CategoryGroupItem = { icon: ( ), - identifier: CATEGORY_IDENTIFIERS.ACTION_INVENTARY, - label: t("filters.categories.actions.forestInventary"), + identifier: CATEGORY_IDENTIFIERS.ACTION_INVENTORY, + label: t("filters.categories.actions.forestInventory"), }; const actionDiversity: CategoryGroupItem = { @@ -111,7 +111,7 @@ export const useCategoriesConfig = (): { }; return { - actions: [actionInventary, actionDiversity, actionSocioEco], + actions: [actionInventory, actionDiversity, actionSocioEco], data: [dataGround, dataSatellite, dataModel], system: [ systemMangroveHigh, diff --git a/webapp/src/features/categories-filters/use-categories-filters.ts b/webapp/src/features/categories-filters/use-categories-filters.ts index f396f000..2ddd7313 100644 --- a/webapp/src/features/categories-filters/use-categories-filters.ts +++ b/webapp/src/features/categories-filters/use-categories-filters.ts @@ -38,7 +38,8 @@ export const useCategoriesFilters = () => { const [categoriesFilters, setCategoriesFilters] = useLocalStorage("categories-filters", { [CATEGORY_IDENTIFIERS.ACTION_DIVERSITY]: true, - [CATEGORY_IDENTIFIERS.ACTION_INVENTARY]: true, + [CATEGORY_IDENTIFIERS.ACTION_INVENTORY]: true, + [CATEGORY_IDENTIFIERS.ACTION_BIO]: true, [CATEGORY_IDENTIFIERS.ACTION_SOCIO]: true, [CATEGORY_IDENTIFIERS.DATA_GROUND]: true, [CATEGORY_IDENTIFIERS.DATA_MODEL]: true, diff --git a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx index 5d4e56ad..20bb4b55 100644 --- a/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx +++ b/webapp/src/features/charts/biodiversity/chart-relative-abundance.tsx @@ -1,7 +1,8 @@ -import { findLabelInExternalData } from "@features/indicators/utils"; -import type { ExternalData } from "@features/popup/forest-inventory/types"; +import { findLabel } from "@features/indicators/utils"; -import { useTranslation } from "@shared/i18n"; +import type { LabelData } from "@entities/data"; + +import { i18nInstance, useTranslation } from "@shared/i18n"; import type { ChartConfig } from "@shared/ui/chart"; import type { ChartComponentType } from "../components/chart-component"; @@ -10,15 +11,16 @@ import { PieChartCategorical } from "../components/pie-chart-categorical"; type PieChartProps = { project: string; data: Record; - externalData: ExternalData; + metadata: LabelData[]; }; export const ChartRelativeAbundance: ChartComponentType = ({ project, data, - externalData, + metadata, }) => { const { t } = useTranslation("all4trees"); + const lang = i18nInstance.language; const smallCategoriesSum = Object.values(data) .filter((value) => value < 5) .reduce((acc, value) => acc + value, 0); @@ -39,13 +41,8 @@ export const ChartRelativeAbundance: ChartComponentType = ({ ...chartConfig, [element.name]: { label: - findLabelInExternalData( - externalData, - "for_label", - project, - "ess", - Number(element.name), - ) || element.name, + findLabel(metadata, project, lang, "ess", Number(element.name)) || + element.name, }, other: { label: t( diff --git a/webapp/src/features/charts/bioinventory/chart-species-status.tsx b/webapp/src/features/charts/bioinventory/chart-species-status.tsx new file mode 100644 index 00000000..0c1935d4 --- /dev/null +++ b/webapp/src/features/charts/bioinventory/chart-species-status.tsx @@ -0,0 +1,75 @@ +import { findStatus } from "@features/indicators/utils"; + +import type { BioSpeciesData } from "@entities/data"; + +import { i18nInstance, useTranslation } from "@shared/i18n"; +import type { ChartConfig } from "@shared/ui/chart"; + +import type { ChartComponentType } from "../components/chart-component"; +import { PieChartCategorical } from "../components/pie-chart-categorical"; + +type PieChartProps = { + data: Record; + metadata: BioSpeciesData[]; + project: string; +}; + +export const ChartSpeciesStatus: ChartComponentType = ({ + data, + metadata, + project, +}) => { + const { t } = useTranslation(["common", "all4trees"]); + const lang = i18nInstance.language; + + // Replace taxon string like 1-2-3 by corresponding status label in external data + const labeledData = Object.entries(data).map<[string, number]>( + ([name, value]) => { + const tax3 = name.split("-")[2]; + const tax3status = ( + tax3 + ? findStatus(metadata, project, lang, Number(tax3)) + : t("common:dataManagement.other") + ) as string; + return [tax3status, value]; + }, + ); + + // Sum pop values grouped by labels then map into chartData + const chartData = Object.entries( + labeledData.reduce( + (acc, [label, value]) => { + if (!acc[label]) { + acc[label] = 0; + } + acc[label] += value; + return acc; + }, + {} as { [key: string]: number }, + ), + ).map(([label, value], index) => ({ + fill: `var(--chart-${(index % 5) + 1})`, + name: label, + value, + })); + + // Generate chart config from chart Data + const chartConfig = chartData.reduce((acc, element) => { + return { + ...acc, + [element.name]: { label: element.name }, + }; + }, {} as ChartConfig); + + return ( + + ); +}; + +ChartSpeciesStatus.isChartComponent = true; diff --git a/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx b/webapp/src/features/charts/components/chart-taxon-abundance.tsx similarity index 89% rename from webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx rename to webapp/src/features/charts/components/chart-taxon-abundance.tsx index 96c817d7..cd8b7daf 100644 --- a/webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx +++ b/webapp/src/features/charts/components/chart-taxon-abundance.tsx @@ -7,11 +7,11 @@ import { type ChartComponentType, } from "@features/charts/components/chart-component"; -import { useTranslation } from "@shared/i18n"; +import { i18nInstance, useTranslation } from "@shared/i18n"; -import { SUNBURST_LAYOUT } from "../config"; -import { buildNodeColors, buildSunburstNodes } from "../lib/sunburst"; -import type { PieChartProps, SunburstTrace } from "../types"; +import { SUNBURST_LAYOUT } from "../soil/config"; +import type { PieChartProps, SunburstTrace } from "../soil/types"; +import { buildNodeColors, buildSunburstNodes } from "./lib/sunburst"; export const ChartTaxonAbundance: ChartComponentType = ({ data, @@ -19,7 +19,7 @@ export const ChartTaxonAbundance: ChartComponentType = ({ project, }) => { const { t } = useTranslation(["common", "all4trees"]); - + const lang = i18nInstance.language; const dataEntries = Object.entries(data); const hasTaxonData = dataEntries.some(([key]) => key.trim() !== ""); let sunburstData: PlotlyData[] = []; @@ -30,7 +30,12 @@ export const ChartTaxonAbundance: ChartComponentType = ({ const filteredDataEntries = dataEntries.filter( ([key]) => key.trim() !== "0", ); - const nodes = buildSunburstNodes(filteredDataEntries, metadata, project); + const nodes = buildSunburstNodes( + filteredDataEntries, + metadata, + project, + lang, + ); const nodeColors = buildNodeColors(nodes); const hoverText = nodes.map( (node) => `${node.label}
${node.value.toFixed(2)} %`, diff --git a/webapp/src/features/charts/soil/lib/sunburst.ts b/webapp/src/features/charts/components/lib/sunburst.ts similarity index 88% rename from webapp/src/features/charts/soil/lib/sunburst.ts rename to webapp/src/features/charts/components/lib/sunburst.ts index 04684fbf..144d6a48 100644 --- a/webapp/src/features/charts/soil/lib/sunburst.ts +++ b/webapp/src/features/charts/components/lib/sunburst.ts @@ -1,16 +1,17 @@ import { darken, lighten } from "color2k"; -import type { ExternalData } from "@features/popup/forest-inventory/types"; +import type { LabelData } from "@entities/data"; import { getChartPalette } from "@shared/lib/palette"; -import type { SunburstNode } from "../types"; +import type { SunburstNode } from "../../soil/types"; import { formatTaxonLevelLabel } from "./taxon"; export function buildSunburstNodes( dataEntries: [string, number][], - metadata: ExternalData, + metadata: LabelData[], project: string, + lang: string, ): SunburstNode[] { const nodes = new Map(); for (const [key, value] of dataEntries) { @@ -18,7 +19,7 @@ export function buildSunburstNodes( nodes.set(key, { depth: parts.length - 1, id: key, - label: formatTaxonLevelLabel(key, metadata, project), + label: formatTaxonLevelLabel(key, metadata, project, lang), parent: parts.slice(0, -1).join("-") || "", value, }); @@ -33,7 +34,7 @@ export function buildSunburstNodes( nodes.set(parentPath, { depth: parentPath.split("-").length - 1, id: parentPath, - label: formatTaxonLevelLabel(parentPath, metadata, project), + label: formatTaxonLevelLabel(parentPath, metadata, project, lang), parent: parts.slice(0, i - 1).join("-") || "", value: value, }); diff --git a/webapp/src/features/charts/soil/lib/taxon.ts b/webapp/src/features/charts/components/lib/taxon.ts similarity index 51% rename from webapp/src/features/charts/soil/lib/taxon.ts rename to webapp/src/features/charts/components/lib/taxon.ts index b6de8068..c2450ce8 100644 --- a/webapp/src/features/charts/soil/lib/taxon.ts +++ b/webapp/src/features/charts/components/lib/taxon.ts @@ -1,48 +1,38 @@ -import { findLabelInExternalData } from "@features/indicators/utils"; -import type { ExternalData } from "@features/popup/forest-inventory/types"; +import { findLabel } from "@features/indicators/utils"; + +import type { LabelData } from "@entities/data"; + +export function getDataTypePrefix(dataType: "tsbf" | "barbA" | null): string { + return dataType ? `${dataType}_` : ""; +} export function getTaxonLabels( element: string, - metadata: ExternalData, + metadata: LabelData[], project: string, + lang: string, ): [string, string, string] { const [taxon1, taxon2, taxon3] = element.split("-"); const taxon1Label = - findLabelInExternalData( - metadata, - "for_label", - project, - "tax1", - Number(taxon1), - ) || taxon1; + findLabel(metadata, project, lang, "tax1", Number(taxon1)) || taxon1; const taxon2Label = - findLabelInExternalData( - metadata, - "for_label", - project, - "tax2", - Number(taxon2), - ) || taxon2; + findLabel(metadata, project, lang, "tax2", Number(taxon2)) || taxon2; const taxon3Label = - findLabelInExternalData( - metadata, - "for_label", - project, - "tax3", - Number(taxon3), - ) || taxon3; + findLabel(metadata, project, lang, "tax3", Number(taxon3)) || taxon3; return [taxon1Label, taxon2Label, taxon3Label]; } export function formatTaxonLevelLabel( element: string, - metadata: ExternalData, + metadata: LabelData[], project: string, + lang: string, ): string { const [taxon1Label, taxon2Label, taxon3Label] = getTaxonLabels( element, metadata, project, + lang, ); const parts = element.split("-"); return parts.length === 1 diff --git a/webapp/src/features/charts/socio-eco/chart-firewood-needs.tsx b/webapp/src/features/charts/socio-eco/chart-firewood-needs.tsx index 3eaa5d62..a6303378 100644 --- a/webapp/src/features/charts/socio-eco/chart-firewood-needs.tsx +++ b/webapp/src/features/charts/socio-eco/chart-firewood-needs.tsx @@ -27,17 +27,17 @@ export const ChartFireWoodNeeds: ChartComponentType = ({ value: data.easyToMeet, }, { - fill: "var(--chart-2)", + fill: "var(--chart-3)", name: "moderateToMeet", value: data.moderateToMeet, }, { - fill: "var(--chart-5)", + fill: "var(--chart-6)", name: "difficultToMeet", value: data.difficultToMeet, }, { - fill: "var(--chart-6)", + fill: "var(--chart-2)", name: "dontKnow", value: data.dontKnow, }, diff --git a/webapp/src/features/charts/socio-eco/chart-living-condition.tsx b/webapp/src/features/charts/socio-eco/chart-living-condition.tsx index ff01cd74..ce8203d3 100644 --- a/webapp/src/features/charts/socio-eco/chart-living-condition.tsx +++ b/webapp/src/features/charts/socio-eco/chart-living-condition.tsx @@ -25,17 +25,17 @@ export const ChartLivingCondition: ChartComponentType = ({ value: data.improvement, }, { - fill: "var(--chart-3)", + fill: "var(--chart-5)", name: "stable", value: data.stable, }, { - fill: "var(--chart-5)", + fill: "var(--chart-6)", name: "regression", value: data.regression, }, { - fill: "var(--chart-6)", + fill: "var(--chart-3)", name: "refuse", value: data.refuse, }, diff --git a/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx b/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx index 29a0fc57..47d26a2c 100644 --- a/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx +++ b/webapp/src/features/charts/socio-eco/chart-timber-needs.tsx @@ -27,17 +27,17 @@ export const ChartTimberNeeds: ChartComponentType = ({ value: data.easyToMeet, }, { - fill: "var(--chart-2)", + fill: "var(--chart-3)", name: "moderateToMeet", value: data.moderateToMeet, }, { - fill: "var(--chart-5)", + fill: "var(--chart-6)", name: "difficultToMeet", value: data.difficultToMeet, }, { - fill: "var(--chart-6)", + fill: "var(--chart-2)", name: "dontKnow", value: data.dontKnow, }, diff --git a/webapp/src/features/charts/soil/types.ts b/webapp/src/features/charts/soil/types.ts index c9b68d3d..96d26c78 100644 --- a/webapp/src/features/charts/soil/types.ts +++ b/webapp/src/features/charts/soil/types.ts @@ -1,8 +1,8 @@ -import type { ExternalData } from "@features/popup/forest-inventory/types"; +import type { LabelData } from "@entities/data"; export type PieChartProps = { data: Record; - metadata: ExternalData; + metadata: LabelData[]; project: string; }; diff --git a/webapp/src/features/controls/layer-control.tsx b/webapp/src/features/controls/layer-control.tsx index 6ced8f24..4789ac60 100644 --- a/webapp/src/features/controls/layer-control.tsx +++ b/webapp/src/features/controls/layer-control.tsx @@ -54,9 +54,9 @@ function useLayerConfig(layerId: string) { /> ), }; - case LAYERS.INVENTARY: + case LAYERS.INVENTORY_FOR: return { - label: t("filters.categories.actions.forestInventary") as string, + label: t("filters.categories.actions.forestInventory") as string, renderIcon: (checked?: boolean) => ( [] = [ + "samp_area", + "density", + "richness", + "taxons_total_pop", +]; + +/** + * Return data in a convenient way for UI rendering, handling units and fixing + */ +export const useFormatBioInventoryData = ( + data: BioInventoryData, + metadata: LabelData[], +) => { + const { t } = useTranslation("common"); + const lang = i18nInstance.language; + const { formatWithUnit } = useFormatterWithUnit(); + + const safeData = preciseNumericIndicators( + data, + indicatorKeys, + t("dataManagement.noData"), + ); + + const taxons_relative_abundance = formatTaxonAbundance( + safeData.taxons_abundance_pop, + safeData.taxons_total_pop, + ); + + return { + area: `${safeData.samp_area} ${ + findLabel( + metadata, + data.proj, + lang, + "unit", + Number(safeData.samp_unit), + ) || t("dataManagement.noUnit") + }`, + density: `${safeData.density} ${safeData.dens_unit || t("dataManagement.noUnit")}`, + relative_abundance: taxons_relative_abundance, + richness: formatWithUnit(safeData.richness, UNITS.speciesInventoried), + taxon: + findLabel(metadata, data.proj, lang, "tax", Number(safeData.taxon)) || + t("dataManagement.noUnit") || + safeData.taxon, + total_population: safeData.taxons_total_pop, + type: + findLabel(metadata, data.proj, lang, "meth", Number(safeData.type)) || + safeData.type, + }; +}; + +export type FormattedData = ReturnType; diff --git a/webapp/src/features/indicators/bio-inventory/index.ts b/webapp/src/features/indicators/bio-inventory/index.ts new file mode 100644 index 00000000..f8a7c812 --- /dev/null +++ b/webapp/src/features/indicators/bio-inventory/index.ts @@ -0,0 +1 @@ +export { useBioInventoryIndicatorElements } from "./use-bioinventory-indicator-elements"; diff --git a/webapp/src/features/indicators/bio-inventory/use-bioinventory-indicator-elements.tsx b/webapp/src/features/indicators/bio-inventory/use-bioinventory-indicator-elements.tsx new file mode 100644 index 00000000..6cd90fec --- /dev/null +++ b/webapp/src/features/indicators/bio-inventory/use-bioinventory-indicator-elements.tsx @@ -0,0 +1,69 @@ +import { Gem, PawPrint, Ruler, SearchAlert, VectorSquare } from "lucide-react"; + +import { ChartSpeciesStatus } from "@features/charts/bioinventory/chart-species-status"; +import { ChartTaxonAbundance } from "@features/charts/components/chart-taxon-abundance"; +import type { BioInventoryData } from "@features/popup/bio-inventory/types"; + +import type { ExternalData } from "@entities/data"; + +import { useTranslation } from "@i18n"; + +import { ICON_SIZE } from "../components/constants"; +import { IndicatorRawValue } from "../components/indicator-raw-value"; +import type { UseIndicatorReturnType } from "../components/types"; +import { useFormatBioInventoryData } from "./format-data"; + +export const useBioInventoryIndicatorElements = ( + rawData: BioInventoryData, + externalData: ExternalData, +): UseIndicatorReturnType => { + const { t } = useTranslation("all4trees"); + const data = useFormatBioInventoryData(rawData, externalData.bio_label); + + return [ + { + children: ( + <> + } + value={data.taxon} + /> + } + value={data.type} + /> + } + value={data.area} + /> + } + value={data.density} + /> + } + value={data.richness} + /> + + + + ), + identifier: "bio", + title: t("indicators.bioinventory.title"), + type: "section", + }, + ]; +}; diff --git a/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx b/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx index 2a4c5114..ca389bad 100644 --- a/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx +++ b/webapp/src/features/indicators/biodiversity/use-biodiversity-indicator-elements.tsx @@ -2,10 +2,9 @@ import { Gem, TreePine, Trees } from "lucide-react"; import { ChartForestPotential } from "@features/charts/biodiversity/chart-forest-potential"; import { ChartRelativeAbundance } from "@features/charts/biodiversity/chart-relative-abundance"; -import type { - ExternalData, - ForestInventoryData, -} from "@features/popup/forest-inventory/types"; +import type { ForestInventoryData } from "@features/popup/forest-inventory/types"; + +import type { LabelData } from "@entities/data"; import { useTranslation } from "@i18n"; @@ -16,7 +15,7 @@ import { useFormatBiodiversityData } from "./format-data"; export const useBiodiversityIndicatorElements = ( rawData: ForestInventoryData, - metadata: ExternalData, + metadata: LabelData[], ): UseIndicatorReturnType => { const { t } = useTranslation("all4trees"); const data = useFormatBiodiversityData(rawData); @@ -46,15 +45,13 @@ export const useBiodiversityIndicatorElements = ( children: ( <> } value={data.treeDiversity.speciesRichness} /> diff --git a/webapp/src/features/indicators/soil/format-data.ts b/webapp/src/features/indicators/soil/format-data.ts index eb60ef17..d4e3ba5f 100644 --- a/webapp/src/features/indicators/soil/format-data.ts +++ b/webapp/src/features/indicators/soil/format-data.ts @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { computeScore, convertDictToPercentage, + formatTaxonAbundance, preciseNumericIndicators, UNITS, useFormatterWithUnit, @@ -21,23 +22,6 @@ const indicatorsToPreciseWithFallBack: NumericKeys[] = [ "soil_eros_stability", ] as const; -function formatTaxonAbundance(abundancePop: string[], abundanceTotal: number) { - if (!abundanceTotal) { - return {}; - } - - const abundancePopRecord: Record = {}; - abundancePop.forEach((value) => { - if (value) { - const [taxon, count] = value.split(":"); - const currentCount = abundancePopRecord[taxon] || 0; - abundancePopRecord[taxon] = currentCount + parseInt(count, 10); - } - }); - - return convertDictToPercentage(abundancePopRecord, abundanceTotal, "0"); -} - /** * Return data in a convenient way for UI rendering, handling units and fixing */ @@ -61,6 +45,23 @@ export const useFormatSoilData = (data: ForestInventoryData) => { t("dataManagement.noData"), ); + safeData.soil_surface_fauna_abundance = convertDictToPercentage( + safeData.soil_surface_fauna_abundance, + Object.values(safeData.soil_surface_fauna_abundance || {}).reduce( + (a, b) => a + b, + 0, + ), + "0", + ); /*formatTaxonAbundance( + safeData.soil_surface_fauna_abundance_pop, + safeData.soil_surface_fauna_total_pop + )*/ + + safeData.soil_fauna_abundance = formatTaxonAbundance( + safeData.soil_fauna_abundance_pop, + safeData.soil_fauna_total_pop, + ); + safeData.soil_surface_fauna_abundance = formatTaxonAbundance( safeData.soil_surface_fauna_abundance_pop, safeData.soil_surface_fauna_total_pop, @@ -87,7 +88,7 @@ export const useFormatSoilData = (data: ForestInventoryData) => { ), soil_fauna_diversity: formatWithUnit( soil_fauna_diversity, - UNITS.speciesCount, + UNITS.speciesPerTrap, ), soil_structure_idx: String(soil_structure_idx) === t("dataManagement.noData") @@ -100,7 +101,7 @@ export const useFormatSoilData = (data: ForestInventoryData) => { ), surface_fauna_diversity: formatWithUnit( soil_surface_fauna_diversity, - UNITS.speciesCount, + UNITS.speciesPerTrap, ), }; }; diff --git a/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx b/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx index 142f04bb..b65a651a 100644 --- a/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx +++ b/webapp/src/features/indicators/soil/use-soil-indicator-elements.tsx @@ -1,12 +1,13 @@ import { Bug, Gem, Sprout } from "lucide-react"; +import { ChartTaxonAbundance } from "@features/charts/components/chart-taxon-abundance"; import { ChartAquaticErosion } from "@features/charts/soil/ui/chart-aquatic-erosion"; -import { ChartTaxonAbundance } from "@features/charts/soil/ui/chart-taxon-abundance"; import { ChartWindErosion } from "@features/charts/soil/ui/chart-wind-erosion"; import type { UseIndicatorReturnType } from "@features/indicators//components/types"; import { IndicatorRawValue } from "@features/indicators/components/indicator-raw-value"; import type { ForestInventoryData } from "@features/popup/forest-inventory"; -import type { ExternalData } from "@features/popup/forest-inventory/types"; + +import type { LabelData } from "@entities/data"; import { useTranslation } from "@i18n"; @@ -15,7 +16,7 @@ import { useFormatSoilData } from "./format-data"; export const useSoilIndicatorElements = ( rawData: ForestInventoryData, - metadata: ExternalData, + metadata: LabelData[], ): UseIndicatorReturnType => { const { t } = useTranslation("all4trees"); diff --git a/webapp/src/features/indicators/utils.ts b/webapp/src/features/indicators/utils.ts index e1151c12..fc8d9236 100644 --- a/webapp/src/features/indicators/utils.ts +++ b/webapp/src/features/indicators/utils.ts @@ -1,7 +1,4 @@ -import type { - ExternalData, - LabelData, -} from "@features/popup/forest-inventory/types"; +import type { BioSpeciesData, LabelData } from "@entities/data"; import { useTranslation } from "@shared/i18n"; import type { LayerMetadata } from "@shared/lib/coordo"; @@ -17,7 +14,8 @@ export const UNITS = { minPerHouseholdPerDay: "minPerHouseholdPerDay", monthPerYear: "monthPerYear", percentFoodRequirements: "percentFoodRequirements", - speciesCount: "speciesCount", + speciesInventoried: "speciesInventoried", + speciesPerTrap: "speciesPerTrap", tonPerHectare: "tonPerHectare", } as const; @@ -56,8 +54,8 @@ export const useFormatterWithUnit = () => { ns: "all4trees", value, }); - case UNITS.speciesCount: - return t("indicators.units.speciesCount", { + case UNITS.speciesPerTrap: + return t("indicators.units.speciesPerTrap", { count: parseInt(formattedValue, 10), ns: "all4trees", }); @@ -66,6 +64,11 @@ export const useFormatterWithUnit = () => { count: parseInt(formattedValue, 10), ns: "all4trees", }); + case UNITS.speciesInventoried: + return t("indicators.units.speciesInventoried", { + count: parseInt(formattedValue, 10), + ns: "all4trees", + }); case UNITS.tonPerHectare: return t("indicators.units.tonPerHectare", { ns: "all4trees", value }); case UNITS.m3PerHabPerYear: @@ -99,7 +102,7 @@ export function preciseNumericIndicators>( Object.entries(data).map(([key, value]) => [ key, indicatorKeys.includes(key as (typeof indicatorKeys)[number]) - ? precise(value, defaultValue) + ? precise(Number(value), defaultValue) : value, // Keep the original value if it's not in the list of indicator keys ]), ) as T; @@ -121,6 +124,35 @@ export function convertDictToPercentage( ); } +/* + Format taxon relative abundance by converting the sring array containing data like 'taxon:pop' by: + 1. Sum all pop corresponding to same taxon + 2. Convert the pop to percentage of total population + 3. Return a record with taxon as key and percentage as value + Example: + Input: ['taxon1:10', 'taxon2:20', 'taxon1:30'], totalPopulation = 60 + Output: { taxon1: 66.67, taxon2: 33.33 } +*/ +export function formatTaxonAbundance( + abundancePop: string[], + abundanceTotal: number, +) { + if (!abundanceTotal) { + return {}; + } + + const abundancePopRecord: Record = {}; + abundancePop.forEach((value) => { + if (value) { + const [taxon, count] = value.split(":"); + const currentCount = abundancePopRecord[taxon] || 0; + abundancePopRecord[taxon] = currentCount + parseInt(count, 10); + } + }); + + return convertDictToPercentage(abundancePopRecord, abundanceTotal, "0"); +} + /* Compute score based on the scale define in for_score external data (see backed/catalog/inventaire_for) Ideally should be conmputed in the config.json but for now we can't do it. @@ -164,16 +196,43 @@ export function findCategoricalLabel( ?.categories?.find((c) => c.value === fieldValue)?.label; } -export function findLabelInExternalData( - externalData: ExternalData, - resourceName: string, +// Find status of corresponding taxon value. +export function findStatus( + resourceData: BioSpeciesData[], + project: string, + lang: string, + taxon: number, +): string | undefined { + if (!resourceData || !Array.isArray(resourceData)) { + return undefined; + } + + // Find the record matching all criteria: project, list_name, and name + const record = resourceData.find((item: BioSpeciesData) => { + return item.proj?.trim() === project.trim() && item.tax3 === taxon; + }); + + return record?.[`stat::${lang}` as keyof BioSpeciesData] as string; +} + +export function findLabel( + resourceData: LabelData[], project: string, + lang: string, fieldName: string, fieldValue: any, ): string | undefined { - // Get the data array for the resource (e.g., for_label, for_mf_tax1, etc.) - const resourceData = externalData[resourceName]; + return findMatchingRecord(resourceData, project, fieldName, fieldValue)?.[ + `label::${lang}` + ]; +} +export function findMatchingRecord( + resourceData: LabelData[], + project: string, + fieldName: string, + fieldValue: any, +): any { if (!resourceData || !Array.isArray(resourceData)) { return undefined; } @@ -182,7 +241,7 @@ export function findLabelInExternalData( const record = resourceData.find((item: LabelData) => { if (typeof item.name !== typeof fieldValue) { console.warn( - `Checking field values with different types ! resourceName=${resourceName} fieldName=${fieldName} fieldValue type=${typeof fieldValue}; item.name type= ${typeof item.name}`, + `Checking field values with different types ! fieldName=${fieldName} fieldValue type=${typeof fieldValue}; item.name type= ${typeof item.name}`, ); } return ( @@ -192,5 +251,5 @@ export function findLabelInExternalData( ); }); - return record?.label; + return record; } diff --git a/webapp/src/features/popup/bio-inventory/index.ts b/webapp/src/features/popup/bio-inventory/index.ts new file mode 100644 index 00000000..56a09ba3 --- /dev/null +++ b/webapp/src/features/popup/bio-inventory/index.ts @@ -0,0 +1,2 @@ +export { BioInventoryPopupContent } from "./popup-bio-inventory"; +export type { BioInventoryData } from "./types"; diff --git a/webapp/src/features/popup/bio-inventory/popup-bio-inventory.tsx b/webapp/src/features/popup/bio-inventory/popup-bio-inventory.tsx new file mode 100644 index 00000000..003ec52b --- /dev/null +++ b/webapp/src/features/popup/bio-inventory/popup-bio-inventory.tsx @@ -0,0 +1,65 @@ +import { cx } from "class-variance-authority"; +import { Leaf } from "lucide-react"; +import { type FC, use } from "react"; + +import { useBioInventoryIndicatorElements } from "@features/indicators/bio-inventory"; +import { ICON_SIZE_HEADER } from "@features/indicators/components/constants"; +import { IndicatorElements } from "@features/indicators/components/indicator-elements"; +import { IndicatorScrollContainer } from "@features/indicators/components/indicator-scroll-container"; +import { findLabel } from "@features/indicators/utils"; +import { IndicatorPopupHeader } from "@features/popup/components/indicator-popup-header"; + +import { formatDate } from "@shared/lib/utils"; +import { i18nInstance, useTranslation } from "@i18n"; + +import type { RenderPopupProps } from "../renderPopup"; +import type { BioInventoryData } from "./types"; + +type BioInventoryPopupContentProps = RenderPopupProps; + +export const BioInventoryPopupContent: FC = ({ + data, + externalDataPromise, + className, + ...headerProps +}) => { + const { t } = useTranslation(["common", "all4trees"]); + const lang = i18nInstance.language; + const externalData = use(externalDataPromise); + + const biodiversityElements = useBioInventoryIndicatorElements( + data, + externalData, + ); + const title = t("popup.bioInventory.title", { + id: data.id, + ns: "all4trees", + }); + + return ( +
+ } + subtitle={ + findLabel( + externalData.bio_label, + data.proj, + lang, + "loc2", + Number(data.forest), + ) || t("dataManagement.undefined", { ns: "common" }) + } + title={title} + {...headerProps} + /> + + + + +
+ ); +}; diff --git a/webapp/src/features/popup/bio-inventory/types.ts b/webapp/src/features/popup/bio-inventory/types.ts new file mode 100644 index 00000000..a43b0ce2 --- /dev/null +++ b/webapp/src/features/popup/bio-inventory/types.ts @@ -0,0 +1,18 @@ +export type BioInventoryData = { + id: number; + conf: boolean; + proj: string; + ecos: number; + region: number; + forest: number; + start_date: Date; + taxon: string; + type: number; + samp_area: number; + samp_unit: number; + density: number; + dens_unit: string; + richness: number; + taxons_abundance_pop: string[]; + taxons_total_pop: number; +}; diff --git a/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx b/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx index 58e3704d..f8b96abe 100644 --- a/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx +++ b/webapp/src/features/popup/forest-inventory/popup-forest-inventory.tsx @@ -8,8 +8,9 @@ import { ICON_SIZE_HEADER } from "@features/indicators/components/constants"; import { IndicatorElements } from "@features/indicators/components/indicator-elements"; import { IndicatorScrollContainer } from "@features/indicators/components/indicator-scroll-container"; import { useSoilIndicatorElements } from "@features/indicators/soil"; -import { findLabelInExternalData } from "@features/indicators/utils"; +import { findLabel } from "@features/indicators/utils"; +import { i18nInstance } from "@shared/i18n"; import { formatDate } from "@shared/lib/utils"; import { GridSelector } from "@shared/ui/grid-selector"; @@ -30,8 +31,16 @@ export const ForestInventoryPopupContent: FC< ForestInventoryPopupContentProps > = ({ data, metadata, externalDataPromise, className, ...headerProps }) => { const { t } = useTranslation(["common", "all4trees"]); + const lang = i18nInstance.language; const [selectedTab, setSelectedTab] = useState(TABS.BIODIVERSITY); const externalData = use(externalDataPromise); + const labelData = externalData.for_label; + + const biodiversityElements = useBiodiversityIndicatorElements( + data, + labelData, + ); + const soilElements = useSoilIndicatorElements(data, labelData); const tabs = { [TABS.BIODIVERSITY]: t("indicators.biodiversity.title", { @@ -48,29 +57,14 @@ export const ForestInventoryPopupContent: FC< }); const subtitle = - findLabelInExternalData( - externalData, - "for_label", - data.project, - "loc2", - data.for, - ) || t("common:dataManagement.undefined"); + findLabel(labelData, data.project, lang, "loc2", data.for) || + t("common:dataManagement.undefined"); const ecos = `${t("all4trees:popup.forestInventory.ecos")}: ${ - findLabelInExternalData( - externalData, - "for_label", - data.project, - "ecos", - data.ecos, - ) || t("dataManagement.undefined", { ns: "common" }) + findLabel(labelData, data.project, lang, "ecos", data.ecos) || + t("dataManagement.undefined", { ns: "common" }) }`; - const biodiversityElements = useBiodiversityIndicatorElements( - data, - externalData, - ); - const soilElements = useSoilIndicatorElements(data, externalData); return (
{ return { diff --git a/webapp/src/shared/api/categories-filters.ts b/webapp/src/shared/api/categories-filters.ts index 660533cf..99e08852 100644 --- a/webapp/src/shared/api/categories-filters.ts +++ b/webapp/src/shared/api/categories-filters.ts @@ -1,4 +1,4 @@ -import { LAYERS } from "./layers"; +import { LAYERS } from "@shared/api/layers"; const SEPARATOR = ":::"; @@ -6,9 +6,10 @@ const SEPARATOR = ":::"; * Format of a layer: GROUP_BACKEND-NAME */ export const CATEGORY_IDENTIFIERS = { + ACTION_BIO: `action${SEPARATOR}${LAYERS.INVENTORY_BIO}`, ACTION_DIVERSITY: "action-tree-diversity", - ACTION_INVENTARY: `action-forest${SEPARATOR}${LAYERS.INVENTARY}`, - ACTION_SOCIO: `action-socio-eco${SEPARATOR}${LAYERS.ENQUETE}`, + ACTION_INVENTORY: `action${SEPARATOR}${LAYERS.INVENTORY_FOR}`, + ACTION_SOCIO: `action${SEPARATOR}${LAYERS.ENQUETE}`, DATA_GROUND: "data-ground", DATA_MODEL: "data-model", DATA_SATELLITE: `data${SEPARATOR}${LAYERS.SATELLITE}`, diff --git a/webapp/src/shared/api/layers.ts b/webapp/src/shared/api/layers.ts index 5ed1eb49..e9169f96 100644 --- a/webapp/src/shared/api/layers.ts +++ b/webapp/src/shared/api/layers.ts @@ -1,7 +1,8 @@ export const LAYERS = { BOUNDARIES: "boundaries", ENQUETE: "enquete", - INVENTARY: "inventaire_for", + INVENTORY_BIO: "inventaire_bio", + INVENTORY_FOR: "inventaire_for", SATELLITE: "satellite", SEED: "seed", SEED_POINT: "seed_point", @@ -11,5 +12,6 @@ export const LAYERS = { export const LAYERS_WITH_CLUSTERS: string[] = [ LAYERS.ENQUETE, - LAYERS.INVENTARY, + LAYERS.INVENTORY_FOR, + LAYERS.INVENTORY_BIO, ]; diff --git a/webapp/src/shared/api/suspense-fetch.ts b/webapp/src/shared/api/suspense-fetch.ts index feda300a..0568d4a2 100644 --- a/webapp/src/shared/api/suspense-fetch.ts +++ b/webapp/src/shared/api/suspense-fetch.ts @@ -18,7 +18,7 @@ import { useCallback, useMemo, useState } from "react"; * ```tsx * const { getDashboardData } = useApi(); * const fetcher = useCallback( - * () => getDashboardData(LAYERS.INVENTARY), + * () => getDashboardData(LAYERS.INVENTORY_FOR), * [getDashboardData], * ); * const { dataPromise, retry } = useSuspenseData({ fetcher }); diff --git a/webapp/src/shared/contexts/map-context-all4trees.ts b/webapp/src/shared/contexts/map-context-all4trees.ts index 5c16b5dd..f248e46a 100644 --- a/webapp/src/shared/contexts/map-context-all4trees.ts +++ b/webapp/src/shared/contexts/map-context-all4trees.ts @@ -3,17 +3,12 @@ import { createContext, type RefCallback, type RefObject } from "react"; import type { CategoriesFiltersState } from "@shared/api/categories-filters"; import type { SetValue } from "@shared/hooks/use-local-storage"; import type { createMap } from "@shared/lib/coordo"; - -export type Category = { value: string; label: string }; - export interface MapContextType { categoriesFilters: CategoriesFiltersState; - forests: Category[]; isReady: boolean; mapApiRef: RefObject | null>; mapContainerRef: RefCallback; setCategoriesFilters: (setter: SetValue) => void; - setForests: (forests: Category[]) => void; setIsReady: (isReady: boolean) => void; } diff --git a/webapp/src/shared/i18n/translations/en/all4trees.json b/webapp/src/shared/i18n/translations/en/all4trees.json index a91856ce..c33a3dd1 100644 --- a/webapp/src/shared/i18n/translations/en/all4trees.json +++ b/webapp/src/shared/i18n/translations/en/all4trees.json @@ -7,7 +7,7 @@ "filters": { "categories": { "actions": { - "forestInventary": "Forest inventory", + "forestInventory": "Forest inventory", "socioEco": "Socio-economic", "title": "Actions taken", "treeDiversity": "Tree diversity" @@ -66,6 +66,13 @@ }, "title": "Biodiversity" }, + "bioinventory": { + "sampArea": "Sampling area", + "status_abundance": "Relative abundance by extinction threat", + "taxon": "Target taxon", + "title": "Fauna inventory", + "type": "Measures type" + }, "common": { "abundance": "Relative abundance", "beneficiary": "Beneficiary", @@ -195,7 +202,9 @@ "title": "Soil" }, "units": { - "essenceCount": "{{ count }} species inventoried", + "essenceCount": "", + "essenceCount_one": "{{ count }} essence inventoried", + "essenceCount_other": "{{ count }} essences inventoried", "individualPerCubicMeter": "{{ value }} ind/m³", "individualPerHectare": "{{ value }} ind/ha", "individualPerTrap": "{{ value }} ind/trap", @@ -203,7 +212,8 @@ "minPerHhPerDay": "{{ value }} min/household/day", "monthPerYear": "{{ value }} month/year", "percentFoodRequirements": "{{ value }} % of food needs", - "speciesCount": "{{ count }} species per trap", + "speciesInventoried": "{{ count }} species inventoried", + "speciesPerTrap": "{{ count }} species per trap", "tonPerHectare": "{{ value }} t/ha" } }, @@ -213,6 +223,10 @@ "villages": "Villages" }, "popup": { + "bioInventory": { + "date": "Survey date: {{ date }}", + "title": "Plot n°{{ id }}" + }, "forestInventory": { "date": "Site survey date : {{ date }}", "ecos": "Ecosystem", diff --git a/webapp/src/shared/i18n/translations/en/common.json b/webapp/src/shared/i18n/translations/en/common.json index 00ddf154..b519af44 100644 --- a/webapp/src/shared/i18n/translations/en/common.json +++ b/webapp/src/shared/i18n/translations/en/common.json @@ -19,6 +19,8 @@ }, "dataManagement": { "noData": "No data available", + "noUnit": "Unit undefined", + "other": "Autre", "undefined": "not found" }, "error": { diff --git a/webapp/src/shared/i18n/translations/fr/all4trees.json b/webapp/src/shared/i18n/translations/fr/all4trees.json index 80dc30d6..762165d8 100644 --- a/webapp/src/shared/i18n/translations/fr/all4trees.json +++ b/webapp/src/shared/i18n/translations/fr/all4trees.json @@ -7,7 +7,7 @@ "filters": { "categories": { "actions": { - "forestInventary": "Inventaire forestier", + "forestInventory": "Inventaire forestier", "socioEco": "Socio-éco", "title": "Actions menées", "treeDiversity": "Diversité arborée" @@ -48,7 +48,6 @@ "other": "Autre", "title": "Abondance relative par essence" }, - "speciesRichness": "Richesse spécifique", "title": "Diversité arborée" }, "tropicalBiodivIndex": { @@ -66,6 +65,13 @@ }, "title": "Biodiversité" }, + "bioinventory": { + "sampArea": "Effort d'échantillonnage", + "status_abundance": "Abondance relative par menace d'extinction", + "taxon": "Taxon cible", + "title": "Inventaire de faune", + "type": "Type de dispositif" + }, "common": { "abundance": "Abondance relative", "beneficiary": "Bénef.", @@ -205,9 +211,12 @@ "minPerHhPerDay": "{{ value }} min/ménage/jour", "monthPerYear": "{{ value }} mois/an", "percentFoodRequirements": "{{ value }} % des besoins alimentaires", - "speciesCount": "", - "speciesCount_one": "{{ count }} espèce par piège", - "speciesCount_other": "{{ count }} espèces par piège", + "speciesInventoried": "", + "speciesInventoried_one": "{{ count }} espèce inventoriée", + "speciesInventoried_other": "{{ count }} espèces inventoriées", + "speciesPerTrap": "", + "speciesPerTrap_one": "{{ count }} espèce par piège", + "speciesPerTrap_other": "{{ count }} espèces par piège", "tonPerHectare": "{{ value }} t/ha" } }, @@ -217,6 +226,10 @@ "villages": "Villages" }, "popup": { + "bioInventory": { + "date": "Date relevé: {{ date }}", + "title": "Placette n°{{ id }}" + }, "forestInventory": { "date": "Date relevé: {{ date }}", "ecos": "Ecosystème", diff --git a/webapp/src/shared/i18n/translations/fr/common.json b/webapp/src/shared/i18n/translations/fr/common.json index 57013943..facf510a 100644 --- a/webapp/src/shared/i18n/translations/fr/common.json +++ b/webapp/src/shared/i18n/translations/fr/common.json @@ -19,6 +19,8 @@ }, "dataManagement": { "noData": "Données non disponibles", + "noUnit": "Unité non définie", + "other": "Autre", "undefined": "non trouvée" }, "error": { diff --git a/webapp/src/widgets/dashboard/dashboard.tsx b/webapp/src/widgets/dashboard/dashboard.tsx index 771a497e..a224170e 100644 --- a/webapp/src/widgets/dashboard/dashboard.tsx +++ b/webapp/src/widgets/dashboard/dashboard.tsx @@ -14,7 +14,7 @@ export default function Dashboard() { // Stable per API client (auth token) so the promise cache stays scoped to // the current session — see suspense-fetch.ts. const fetcher = useCallback( - () => getDashboardData(LAYERS.INVENTARY), + () => getDashboardData(LAYERS.INVENTORY_FOR), [getDashboardData], ); const { dataPromise, retry } = useSuspenseData({ fetcher }); diff --git a/webapp/src/widgets/map/assets/bio-inventory-icon.svg b/webapp/src/widgets/map/assets/bio-inventory-icon.svg new file mode 100644 index 00000000..a430b787 --- /dev/null +++ b/webapp/src/widgets/map/assets/bio-inventory-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/webapp/src/widgets/map/assets/inventaire-icon.svg b/webapp/src/widgets/map/assets/forest-inventory-icon.svg similarity index 100% rename from webapp/src/widgets/map/assets/inventaire-icon.svg rename to webapp/src/widgets/map/assets/forest-inventory-icon.svg diff --git a/webapp/src/widgets/map/map-all4trees.tsx b/webapp/src/widgets/map/map-all4trees.tsx index ce7544b3..e2f9ca5f 100644 --- a/webapp/src/widgets/map/map-all4trees.tsx +++ b/webapp/src/widgets/map/map-all4trees.tsx @@ -1,6 +1,10 @@ import { type FC, useEffect, useState } from "react"; import { type SeedData, SeedIndicator } from "@features/indicators/seed"; +import { + type BioInventoryData, + BioInventoryPopupContent, +} from "@features/popup/bio-inventory"; import { type ForestInventoryData, ForestInventoryPopupContent, @@ -18,13 +22,14 @@ import { LAYERS } from "@shared/api/layers"; import { useMap } from "@shared/hooks/use-map-all4trees"; import { useApi } from "@shared/hooks/useApi"; -import pictoInventaire from "./assets/inventaire-icon.svg"; +import pictoBioInventory from "./assets/bio-inventory-icon.svg"; +import pictoForestInventory from "./assets/forest-inventory-icon.svg"; import pictoSocioEco from "./assets/socio-eco-icon.svg"; import { MapBase } from "./map-base"; import { getExternalDataPromiseByLayer, getIconSize } from "./utils"; export const MapAll4Trees: FC = () => { - const { isReady, mapApiRef, forests, mapContainerRef } = useMap(); + const { isReady, mapApiRef, mapContainerRef } = useMap(); // We need to pass the api as prop to the popup content, so we can fetch data from the catalog. // We can't call useApi() from inside the popup as it is created dynamically and not part of the React tree. const api = useApi(); @@ -36,8 +41,8 @@ export const MapAll4Trees: FC = () => { mapApiRef.current.setLayerSymbol({ iconSize: getIconSize({}), - layerId: LAYERS.INVENTARY, - svg: pictoInventaire, + layerId: LAYERS.INVENTORY_FOR, + svg: pictoForestInventory, }); mapApiRef.current.setLayerSymbol({ @@ -46,14 +51,23 @@ export const MapAll4Trees: FC = () => { svg: pictoSocioEco, }); - // Set the popup for the "inventaire" layer + mapApiRef.current.setLayerSymbol({ + iconSize: getIconSize({}), + layerId: LAYERS.INVENTORY_BIO, + svg: pictoBioInventory, + }); + + // Set the popup for the "inventaire_for" layer mapApiRef.current.setLayerPopup({ centerOnClick: true, - layerId: LAYERS.INVENTARY, + layerId: LAYERS.INVENTORY_FOR, popupConfig: DEFAULT_POPUP_CONFIG, renderCallback: getRenderPopupLayer({ Element: ForestInventoryPopupContent, - getExternalData: getExternalDataPromiseByLayer(LAYERS.INVENTARY, api), + getExternalData: getExternalDataPromiseByLayer( + LAYERS.INVENTORY_FOR, + api, + ), toggleShiftSize, }), trigger: "click", @@ -72,6 +86,22 @@ export const MapAll4Trees: FC = () => { trigger: "click", }); + // Set the popup for the "inventaire_bio" layer + mapApiRef.current.setLayerPopup({ + centerOnClick: true, + layerId: LAYERS.INVENTORY_BIO, + popupConfig: DEFAULT_POPUP_CONFIG, + renderCallback: getRenderPopupLayer({ + Element: BioInventoryPopupContent, + getExternalData: getExternalDataPromiseByLayer( + LAYERS.INVENTORY_BIO, + api, + ), + toggleShiftSize, + }), + trigger: "click", + }); + // Set the popup for the Seed data layer mapApiRef.current.setLayerPopup({ centerOnClick: true, @@ -86,45 +116,11 @@ export const MapAll4Trees: FC = () => { }); }, [isReady, mapApiRef, api]); - const filterByForest = (forestId: string) => { - mapApiRef.current?.setLayerFilters({ - filters: { args: [{ property: "for" }, forestId], op: "=" }, - layerId: "inventaire", - }); - }; - - const resetFilter = () => { - mapApiRef.current?.setLayerFilters({ - filters: null, - layerId: "inventaire", - }); - }; - return ( - {forests.length > 0 && ( -
- {forests.map((forest) => ( - - ))} - -
- )} -
+ /> ); }; diff --git a/webapp/src/widgets/map/utils.ts b/webapp/src/widgets/map/utils.ts index 089258f5..728e65e3 100644 --- a/webapp/src/widgets/map/utils.ts +++ b/webapp/src/widgets/map/utils.ts @@ -1,5 +1,6 @@ +import { EXTERNAL_RESOURCES_BY_LAYER } from "@entities/resources"; + import type { ApiClient } from "@shared/api/client"; -import { LAYERS } from "@shared/api/layers"; export const SVG_SIZE_DEFAULT = 72; export const TARGET_SIZE_DEFAULT = 48; @@ -17,15 +18,6 @@ export function getIconSize({ return targetSize / assetSize; } -export const EXTERNAL_RESOURCES_BY_LAYER = new Map([ - [ - LAYERS.INVENTARY, - ["for_label", "for_mf_tax1", "for_mf_tax2", "for_mf_tax3", "for_score"], - ], - [LAYERS.ENQUETE, [""]], - [LAYERS.SEED_POINT, [""]], -]); - export const getExternalDataPromiseByLayer = ( layerId: string, client: ApiClient,