Voir le code
library(tidyverse)
library(sf)
library(yaml)
library(wdpar)
library(lubridate)
# Load WDPA reference
wdpa_mdg <- wdpa_read("data/WDPA_WDOECM_Nov2025_Public_MDG.zip")Le CAR (Consolidation and Auditing Rules) transforme les données de base WDPA et les amendements YAML en un jeu de données temporel et spatialement explicite. Chaque aire protégée est représentée comme une séquence d’états délimités dans le temps, chaque état pouvant contenir plusieurs zones (limite externe, noyaux durs, zones tampons).
Principes fondamentaux :
Types d’amendements reconnus :
status_change : Changement de statut historique (période close uniquement, le statut actuel est dans WDPA)correction : Correction d’un attribut WDPA erroné (peut être open-ended)boundary_modification : Géométrie historique différente de la géométrie WDPA actuellesecondary_zoning : Zonage secondaire (noyaux durs, zones tampons)temporary_protection : Mise en protection temporaire (avant désignation permanente)Structure de sortie :
state_id | WDPAID | valid_from | valid_to | zone_type | zone_name | geometry | [WDPA attributes] | amendment_source
Chaque ligne représente une zone dans un état temporel. Une AP avec des modifications de limites et un zonage secondaire aura plusieurs lignes par période.
library(tidyverse)
library(sf)
library(yaml)
library(wdpar)
library(lubridate)
# Load WDPA reference
wdpa_mdg <- wdpa_read("data/WDPA_WDOECM_Nov2025_Public_MDG.zip")Le registre d’amendements consiste en fichiers YAML (métadonnées) et fichiers GeoJSON (géométries) créés par 07_curation.qmd.
#' Read YAML amendment registry
#'
#' Parses all YAML files in the amendments directory and returns a structured
#' tibble with amendment metadata. Spatial amendments include geometry references.
#'
#' @param dir Path to amendments directory
#' @return List with components:
#' - all: All amendments as tibble
#' - spatial: Spatial amendments only
#' - attribute: Attribute amendments only
read_yaml_amendments <- function(dir = "data/amendments") {
yaml_files <- list.files(dir, pattern = "\\.yml$", full.names = TRUE)
if (length(yaml_files) == 0) {
stop("No YAML files found in ", dir)
}
# Parse each YAML
amendments <- map_dfr(yaml_files, function(yaml_path) {
a <- yaml::read_yaml(yaml_path)
# Required fields validation
required <- c("amendment_id", "wdpaid", "amendment_kind", "amendment_type")
missing <- setdiff(required, names(a))
if (length(missing) > 0) {
stop(
"Missing required fields in ",
basename(yaml_path),
": ",
paste(missing, collapse = ", ")
)
}
tibble(
amendment_id = a$amendment_id,
wdpaid = a$wdpaid,
wdpa_name = a$wdpa_name %||% NA_character_,
amendment_kind = a$amendment_kind,
amendment_type = a$amendment_type,
# Temporal bounds
valid_from = if (!is.null(a$valid_from)) {
as.Date(a$valid_from)
} else {
as.Date(NA)
},
valid_to = if (!is.null(a$valid_to)) as.Date(a$valid_to) else as.Date(NA),
# Legal instrument
legal_source = a$legal_instrument$source %||% NA_character_,
legal_number = a$legal_instrument$number %||% NA_character_,
legal_date = if (!is.null(a$legal_instrument$date)) {
as.Date(a$legal_instrument$date)
} else {
as.Date(NA)
},
legal_url = a$legal_instrument$url %||% NA_character_,
notes = a$notes %||% NA_character_,
# Spatial-specific fields
geometry_ref = a$geometry_ref %||% NA_character_,
geometry_source_dataset = a$geometry_source_dataset_id %||% NA_character_,
geom_file = if (!is.null(a$geometry_ref)) {
file.path(dir, a$geometry_ref)
} else {
NA_character_
},
# Attribute-specific fields (store as list column)
attributes = if (!is.null(a$attributes)) {
list(a$attributes)
} else {
list(NULL)
},
# Provenance
yaml_path = yaml_path
)
})
# Split by kind
list(
all = amendments,
spatial = amendments |> filter(amendment_kind == "spatial"),
attribute = amendments |> filter(amendment_kind == "attribute")
)
}
# Load amendments
amendments <- read_yaml_amendments("data/amendments")
# Summary
cat("Amendment registry loaded:\n")Amendment registry loaded:
cat(" Total amendments:", nrow(amendments$all), "\n") Total amendments: 106
cat(" Spatial:", nrow(amendments$spatial), "\n") Spatial: 35
cat(" Attribute:", nrow(amendments$attribute), "\n") Attribute: 71
cat("\nAmendment types:\n")
Amendment types:
amendments$all |>
count(amendment_kind, amendment_type) |>
print()# A tibble: 5 × 3
amendment_kind amendment_type n
<chr> <chr> <int>
1 attribute correction 16
2 attribute status_change 13
3 attribute temporary_protection 42
4 spatial boundary_modification 18
5 spatial secondary_zoning 17
Création d’intervalles temporels non chevauchants en combinant tous les marqueurs de dates issus de WDPA et des amendements.
#' Generate timeline breakpoints for a protected area
#'
#' Collects all temporal markers (STATUS_YR from WDPA, valid_from/valid_to from
#' amendments) and creates non-overlapping intervals.
#'
#' @param wdpaid_val WDPAID to generate timeline for
#' @param wdpa WDPA reference dataset
#' @param amendments Amendment registry (from read_yaml_amendments)
#' @return Tibble with interval_start and interval_end columns
generate_timeline <- function(wdpaid_val, wdpa, amendments) {
# Get WDPA record (may be absent for dissolved/merged PAs)
wdpa_record <- wdpa |> filter(WDPAID == wdpaid_val)
# Collect all dates from amendments
pa_amendments <- amendments$all |> filter(wdpaid == wdpaid_val)
if (nrow(wdpa_record) == 0 && nrow(pa_amendments) == 0) {
stop("WDPAID ", wdpaid_val, " not found in WDPA and has no amendments")
}
# Amendment dates (authoritative when present)
amendment_dates <- c(
pa_amendments$valid_from[!is.na(pa_amendments$valid_from)],
pa_amendments$valid_to[!is.na(pa_amendments$valid_to)]
)
# STATUS_YR as fallback: only used when no amendments provide dated history.
# When amendments exist, their valid_from/valid_to already capture the real
# temporal breakpoints. Using STATUS_YR in that case would create spurious
# intervals (STATUS_YR is the year of the last WDPA update, not a historical
# event date).
wdpa_date <- if (nrow(wdpa_record) > 0 && length(amendment_dates) == 0) {
as.Date(paste0(wdpa_record$STATUS_YR[1], "-01-01"))
}
dates <- c(wdpa_date, amendment_dates) |>
unique() |>
sort()
# Restore Date class if lost during c() / unique() / sort()
dates <- as.Date(dates, origin = "1970-01-01")
# Créer les intervalles
tibble(
interval_start = dates,
interval_end = c(dates[-1], as.Date(NA)) # Dernier intervalle ouvert
)
}Précédence spatiale :
boundary_modification : La géométrie de l’amendement remplace WDPA pendant sa période de validitésecondary_zoning : Additif (crée des zones supplémentaires en plus de la limite)Précédence des attributs :
#' Find active amendments for a time interval
#'
#' Returns amendments whose validity period overlaps with [interval_start, interval_end).
#' An amendment is active if:
#' - It starts before the interval ends (or interval is open-ended)
#' - It ends after the interval starts (or amendment is open-ended)
#'
#' @param amendments_df Amendment registry subset (spatial or attribute)
#' @param wdpaid_val WDPAID to filter
#' @param interval_start Start of interval (inclusive)
#' @param interval_end End of interval (exclusive), NA for open-ended
#' @return Filtered tibble of active amendments
find_active_amendments <- function(
amendments_df,
wdpaid_val,
interval_start,
interval_end
) {
# Replace NA interval_end with far-future date for comparison
effective_end <- if (is.na(interval_end)) {
as.Date("2100-01-01")
} else {
interval_end
}
amendments_df |>
filter(wdpaid == wdpaid_val) |>
filter(
# Amendment starts before interval ends
(is.na(valid_from) | valid_from < effective_end) &
# Amendment ends after interval starts
(is.na(valid_to) | valid_to > interval_start)
)
}
#' Check for conflicts in amendments
#'
#' Detects multiple amendments affecting the same attribute/geometry in the same
#' period. temporary_protection amendments are treated as attribute amendments
#' for conflict purposes.
#'
#' @param amendments_active Active amendments for an interval
#' @param type "boundary" or "attribute"
#' @return FALSE if no conflict (throws error on conflict)
check_conflicts <- function(amendments_active, type = "boundary") {
if (type == "boundary") {
# Multiple boundary_modifications in same period
boundary_mods <- amendments_active |>
filter(amendment_type == "boundary_modification")
if (nrow(boundary_mods) > 1) {
stop(
"Conflict: Multiple boundary_modification amendments active in same period:\n",
paste(boundary_mods$amendment_id, collapse = "\n")
)
}
} else if (type == "attribute") {
# Filter to amendments that actually have non-null attributes
attr_amendments <- amendments_active |>
filter(map_lgl(attributes, ~ !is.null(.x) && length(.x) > 0))
if (nrow(attr_amendments) > 1) {
# Check for overlapping attributes
all_attr_names <- map(attr_amendments$attributes, names) |>
unlist()
duplicated_attrs <- all_attr_names[duplicated(all_attr_names)]
if (length(duplicated_attrs) > 0) {
for (attr_name in unique(duplicated_attrs)) {
conflicting <- attr_amendments |>
filter(map_lgl(attributes, ~ attr_name %in% names(.x)))
# Extract values for this attribute across conflicting amendments
values <- map_chr(
conflicting$attributes,
~ as.character(.x[[attr_name]])
)
# Only error if values actually differ (identical values = no conflict)
if (n_distinct(values) > 1) {
stop(
"Conflict: Multiple amendments set different values for '",
attr_name,
"' in same period:\n",
paste(
conflicting$amendment_id,
"→",
values,
collapse = "\n"
)
)
}
}
}
}
}
FALSE
}Application des amendements à la base WDPA pour chaque intervalle temporel, créant des enregistrements de zones.
#' Standardize types for dynamic WDPA states
#'
#' Converts all WDPA attribute columns to character for consistency across
#' amended and non-amended states. Preserves special columns (dates, IDs, geometry).
#'
#' @param df A data frame or sf object with WDPA columns
#' @return Same object with standardized types
standardize_types <- function(df) {
# Columns that should remain as-is (dates, identifiers, geometry)
preserve_cols <- c(
"valid_from",
"valid_to",
"state_id",
"zone_type",
"zone_name",
"amendment_source",
"geometry"
)
# Convert all WDPA columns (not in preserve list) to character
# Use vapply + sprintf to avoid R 4.5 prettyNum() / format.default() issues
num_cols <- names(df)[map_lgl(df, is.numeric)]
num_cols <- setdiff(num_cols, preserve_cols)
for (col in num_cols) {
vals <- df[[col]]
# Integer-like values: no decimal; true doubles: full precision
df[[col]] <- ifelse(
is.na(vals),
NA_character_,
ifelse(
vals == trunc(vals),
sprintf("%.0f", vals),
sprintf("%g", vals)
)
)
}
df
}
#' Consolidate PA states by applying amendments
#'
#' Main function that generates temporal states with zones for a single
#' protected area. Implements the "delta-only" model: amendments only document
#' differences from WDPA. The current period always inherits from WDPA unless
#' a correction amendment exists.
#'
#' @param wdpaid WDPAID to process
#' @param wdpa WDPA reference dataset
#' @param amendments Amendment registry
#' @return sf object with one row per zone per temporal state
consolidate_pa_states <- function(wdpaid, wdpa, amendments) {
# Get WDPA baseline (may be absent for dissolved/merged PAs)
wdpa_record <- wdpa |> filter(WDPAID == wdpaid)
has_wdpa <- nrow(wdpa_record) > 0
# Generate timeline
timeline <- generate_timeline(wdpaid, wdpa, amendments)
# For each interval, create state(s)
states <- map_dfr(seq_len(nrow(timeline)), function(i) {
interval_start <- timeline$interval_start[i]
interval_end <- timeline$interval_end[i]
# Find active amendments
active_spatial <- find_active_amendments(
amendments$spatial,
wdpaid,
interval_start,
interval_end
)
active_attribute <- find_active_amendments(
amendments$attribute,
wdpaid,
interval_start,
interval_end
)
# Check conflicts
check_conflicts(active_spatial, "boundary")
check_conflicts(active_attribute, "attribute")
# Start with WDPA attributes or minimal skeleton for dissolved PAs
if (has_wdpa) {
state_attrs <- wdpa_record |>
st_drop_geometry() |>
standardize_types()
} else {
# Dissolved/merged PA: build minimal skeleton from amendment metadata
pa_meta <- amendments$all |> filter(wdpaid == !!wdpaid) |> slice(1)
state_attrs <- tibble(
WDPAID = as.character(wdpaid),
NAME = pa_meta$wdpa_name %||% as.character(wdpaid),
ISO3 = pa_meta$amendment_id |>
str_extract("^[A-Z]{3}") %||%
NA_character_
)
}
# Apply attribute amendments (status_change, correction, temporary_protection)
# Only amendments active in this interval override WDPA attributes.
# For the open period, WDPA baseline is used unless a correction exists.
if (nrow(active_attribute) > 0) {
for (j in seq_len(nrow(active_attribute))) {
attrs <- active_attribute$attributes[[j]]
if (!is.null(attrs)) {
for (attr_name in names(attrs)) {
state_attrs[[attr_name]] <- as.character(attrs[[attr_name]])
}
}
}
}
# Derive STATUS_YR from context:
# Per WDPA manual, STATUS_YR = year the *current status* came into effect.
#
# Two cases:
# 1. A status_change/temporary_protection is active in this interval:
# → STATUS_YR = year(valid_from) of that amendment (= when this
# historical status started).
# 2. No status amendment active in this interval:
# → STATUS_YR comes from WDPA baseline or from a `correction` amendment
# if one exists. Corrections are open-ended and already applied above.
# When WDPA eventually fixes its STATUS_YR, the correction simply
# overwrites with the same value (idempotent, no conflict).
status_amendments <- active_attribute |>
filter(amendment_type %in% c("status_change", "temporary_protection"))
if (nrow(status_amendments) > 0) {
# Case 1: active status amendment → year of its valid_from
status_yr_date <- max(status_amendments$valid_from, na.rm = TRUE)
state_attrs[["STATUS_YR"]] <- as.character(year(status_yr_date))
}
# Case 2: no override needed — WDPA or correction already in state_attrs
# Pre-compute state_id outside mutate to avoid R 4.5 format()/prettyNum()
# interaction issues inside dplyr context
current_state_id <- paste0(wdpaid, "_", year(interval_start))
# Create zones
zones <- list()
# Zone 1: External boundary
boundary_mod <- active_spatial |>
filter(amendment_type == "boundary_modification")
if (nrow(boundary_mod) > 0) {
# Use amended boundary
geom <- st_read(boundary_mod$geom_file[1], quiet = TRUE)
zones[[1]] <- state_attrs |>
mutate(
state_id = current_state_id,
valid_from = interval_start,
valid_to = interval_end,
zone_type = "external_boundary",
zone_name = NA_character_,
amendment_source = boundary_mod$amendment_id[1],
geometry = st_geometry(geom)[1]
) |>
st_as_sf()
} else if (has_wdpa) {
# Use WDPA boundary with standardized types
zones[[1]] <- wdpa_record |>
standardize_types() |>
mutate(
state_id = current_state_id,
valid_from = interval_start,
valid_to = interval_end,
zone_type = "external_boundary",
zone_name = NA_character_,
amendment_source = "WDPA"
)
} else {
# Dissolved PA without boundary amendment for this period: skip zone
warning(
"WDPAID ",
wdpaid,
": no WDPA record and no boundary_modification ",
"for interval [",
interval_start,
", ",
interval_end,
")"
)
zones[[1]] <- state_attrs |>
mutate(
state_id = current_state_id,
valid_from = interval_start,
valid_to = interval_end,
zone_type = "external_boundary",
zone_name = NA_character_,
amendment_source = "amendments_only",
geometry = st_sfc(st_polygon(), crs = 4326)
) |>
st_as_sf()
}
# Inherit amended attributes into boundary zone
for (col in names(state_attrs)) {
if (col %in% names(zones[[1]]) && col != "WDPAID") {
zones[[1]][[col]] <- state_attrs[[col]]
}
}
# Additional zones: Secondary zoning (may contain multiple features)
secondary_zones <- active_spatial |>
filter(amendment_type == "secondary_zoning")
if (nrow(secondary_zones) > 0) {
for (j in seq_len(nrow(secondary_zones))) {
geom <- st_read(secondary_zones$geom_file[j], quiet = TRUE)
n_features <- nrow(geom)
for (k in seq_len(n_features)) {
zone_name_val <- if ("zone_name" %in% names(geom)) {
geom$zone_name[k]
} else if ("name" %in% names(geom)) {
geom$name[k]
} else {
paste0("zone_", k)
}
zone <- state_attrs |>
mutate(
state_id = current_state_id,
valid_from = interval_start,
valid_to = interval_end,
zone_type = "secondary_zoning",
zone_name = zone_name_val,
amendment_source = secondary_zones$amendment_id[j],
geometry = st_geometry(geom)[k]
) |>
st_as_sf()
zones[[length(zones) + 1]] <- zone
}
}
}
# Combine all zones for this state
bind_rows(zones)
})
# Restore Date class lost during bind_rows() across heterogeneous sf/tibble
states$valid_from <- as.Date(states$valid_from, origin = "1970-01-01")
states$valid_to <- as.Date(states$valid_to, origin = "1970-01-01")
states
}Fonction simple pour visualiser l’évolution temporelle d’une AP.
#' Print timeline for a protected area
#'
#' Shows temporal states and amendments in chronological order for human review.
#'
#' @param wdpaid WDPAID to print timeline for
#' @param states Consolidated states (output from consolidate_pa_states)
#' @return Prints formatted timeline to console
print_timeline <- function(wdpaid, states) {
pa_states <- states |>
filter(WDPAID == wdpaid) |>
st_drop_geometry() |>
arrange(valid_from, zone_type)
if (nrow(pa_states) == 0) {
cat("Aucun état trouvé pour WDPAID", wdpaid, "\n")
return(invisible(NULL))
}
pa_name <- pa_states$NAME[1]
cat("\n")
cat("═══════════════════════════════════════════════════════════════\n")
cat("TIMELINE:", pa_name, "(WDPAID:", wdpaid, ")\n")
cat("═══════════════════════════════════════════════════════════════\n\n")
current_state_id <- NULL
for (i in seq_len(nrow(pa_states))) {
row <- pa_states[i, ]
# Print state header if new state
if (is.null(current_state_id) || row$state_id != current_state_id) {
current_state_id <- row$state_id
period <- if (is.na(row$valid_to)) {
paste0(
format.Date(row$valid_from, "%Y-%m-%d"),
" to present"
)
} else {
paste0(
format.Date(row$valid_from, "%Y-%m-%d"),
" to ",
format.Date(row$valid_to, "%Y-%m-%d")
)
}
cat("┌─", period, "\n")
cat("│ Status:", row$DESIG, "(", row$IUCN_CAT, ")\n")
if (!is.na(row$STATUS_YR)) {
cat("│ STATUS_YR:", row$STATUS_YR, "\n")
}
}
# Print zone info
cat("│ ├─ Zone:", row$zone_type)
if (!is.na(row$zone_name)) {
cat(" (", row$zone_name, ")")
}
cat("\n")
cat("│ │ Source:", row$amendment_source, "\n")
}
cat("└─────────────────────────────────────────────────────────────\n\n")
invisible(NULL)
}Test de consolidation sur Ankarafantsika (a des changements de statut et modifications de limites).
# Consolidate one PA
test_states <- consolidate_pa_states(
wdpaid = 1299, # Ankarafantsika
wdpa = wdpa_mdg,
amendments = amendments
)
# Print timeline
print_timeline(1299, test_states)
═══════════════════════════════════════════════════════════════
TIMELINE: Ankarafantsika (WDPAID: 1299 )
═══════════════════════════════════════════════════════════════
┌─ 1927-12-31 to 2002-08-07
│ Status: Réserve Naturelle Intégrale ( Ia )
│ STATUS_YR: 1927
│ ├─ Zone: external_boundary
│ │ Source: WDPA
┌─ 2002-08-07 to 2015-04-21
│ Status: Parc National ( II )
│ STATUS_YR: 2002
│ ├─ Zone: external_boundary
│ │ Source: MDG-1299-2015-boundary_modification-001
┌─ 2015-04-21 to present
│ Status: Parc National ( II )
│ STATUS_YR: 2002
│ ├─ Zone: external_boundary
│ │ Source: WDPA
└─────────────────────────────────────────────────────────────
# Inspect structure
test_states |>
st_drop_geometry() |>
select(
state_id,
valid_from,
valid_to,
zone_type,
DESIG,
STATUS_YR,
amendment_source
)# A tibble: 3 × 7
state_id valid_from valid_to zone_type DESIG STATUS_YR amendment_source
<chr> <date> <date> <chr> <chr> <chr> <chr>
1 1299_1927 1927-12-31 2002-08-07 external_bou… Rése… 1927 WDPA
2 1299_2002 2002-08-07 2015-04-21 external_bou… Parc… 2002 MDG-1299-2015-b…
3 1299_2015 2015-04-21 NA external_bou… Parc… 2002 WDPA
Détection des problèmes potentiels dans le registre d’amendements avant consolidation complète.
# 1. Pas de status_change open-ended (sans valid_to)
open_status_change <- amendments$attribute |>
filter(amendment_type == "status_change", is.na(valid_to))
if (nrow(open_status_change) > 0) {
cat("\u26a0\ufe0f status_change open-ended (ne devrait pas exister) :\n")
open_status_change |>
select(amendment_id, wdpaid, wdpa_name, valid_from) |>
print()
} else {
cat("\u2705 Pas de status_change open-ended\n")
}✅ Pas de status_change open-ended
# 2. temporary_protection avec date permanente connue doit avoir valid_to
temp_prot <- amendments$attribute |>
filter(amendment_type == "temporary_protection")
if (nrow(temp_prot) > 0) {
temp_open <- temp_prot |> filter(is.na(valid_to))
temp_closed <- temp_prot |> filter(!is.na(valid_to))
cat("\u2705 Protections temporaires :", nrow(temp_prot), "au total\n")
cat(" - Closes (désignation permanente obtenue) :", nrow(temp_closed), "\n")
cat(" - Ouvertes (encore temporaire) :", nrow(temp_open), "\n")
}✅ Protections temporaires : 42 au total
- Closes (désignation permanente obtenue) : 34
- Ouvertes (encore temporaire) : 8
# 3. Pas de STATUS_YR dans les attributs des status_change
status_yr_in_change <- amendments$attribute |>
filter(amendment_type == "status_change") |>
filter(map_lgl(attributes, ~ "STATUS_YR" %in% names(.x %||% list())))
if (nrow(status_yr_in_change) > 0) {
cat("\u26a0\ufe0f status_change avec STATUS_YR (à supprimer) :\n")
status_yr_in_change |>
select(amendment_id, wdpaid) |>
print()
} else {
cat("\u2705 Pas de STATUS_YR dans les status_change\n")
}⚠️ status_change avec STATUS_YR (à supprimer) :
# A tibble: 1 × 2
amendment_id wdpaid
<chr> <int>
1 MDG-5025-2015-status_change-001 5025
# 4. Pas de NODATE dans les amendment_id
nodate_amendments <- amendments$all |>
filter(str_detect(amendment_id, "NODATE"))
if (nrow(nodate_amendments) > 0) {
cat("\u26a0\ufe0f Amendements avec NODATE dans l'ID :\n")
nodate_amendments |>
select(amendment_id, wdpaid, amendment_type, valid_from, valid_to) |>
print()
} else {
cat("\u2705 Pas d'amendment_id avec NODATE\n")
}⚠️ Amendements avec NODATE dans l'ID :
# A tibble: 33 × 5
amendment_id wdpaid amendment_type valid_from valid_to
<chr> <int> <chr> <date> <date>
1 MDG-1299-NODATE-correction-001 1299 correction 2002-08-07 NA
2 MDG-166880-NODATE-secondary_zoni… 166880 secondary_zon… NA NA
3 MDG-20272-NODATE-correction-001 20272 correction 1997-12-18 NA
4 MDG-2303-NODATE-correction-001 2303 correction 1997-08-07 NA
5 MDG-2306-NODATE-correction-001 2306 correction 1927-12-31 NA
6 MDG-2306-NODATE-correction-002 2306 correction 2015-04-28 NA
7 MDG-2307-NODATE-correction-001 2307 correction 2002-08-07 NA
8 MDG-2309-NODATE-correction-001 2309 correction 2002-08-07 NA
9 MDG-2310-NODATE-correction-001 2310 correction 1927-12-31 NA
10 MDG-303702-NODATE-correction-001 303702 correction 2011-09-06 NA
# ℹ 23 more rows
Application à toutes les AP de Madagascar.
# Get unique WDPAIDs that have amendments
wdpaids_with_amendments <- unique(amendments$all$wdpaid)
cat("Processing", length(wdpaids_with_amendments), "PAs with amendments...\n")Processing 68 PAs with amendments...
# Consolidate all
dynamic_wdpa <- map_dfr(
wdpaids_with_amendments,
function(wdpaid) {
tryCatch(
{
consolidate_pa_states(wdpaid, wdpa_mdg, amendments)
},
error = function(e) {
warning("Error processing WDPAID ", wdpaid, ": ", e$message)
NULL
}
)
},
.progress = TRUE
)
# Add PAs without amendments (just WDPA baseline)
wdpaids_without_amendments <- setdiff(wdpa_mdg$WDPAID, wdpaids_with_amendments)
baseline_states <- wdpa_mdg |>
filter(WDPAID %in% wdpaids_without_amendments) |>
standardize_types() |> # Use standard type conversion
mutate(
state_id = paste0(WDPAID, "_", STATUS_YR),
valid_from = as.Date(paste0(STATUS_YR, "-01-01")),
valid_to = as.Date(NA),
zone_type = "external_boundary",
zone_name = NA_character_,
amendment_source = "WDPA"
)
# Combine
dynamic_wdpa_full <- bind_rows(dynamic_wdpa, baseline_states)
# Summary
cat("\nDynamic WDPA generated:\n")
Dynamic WDPA generated:
cat(" Total states:", n_distinct(dynamic_wdpa_full$state_id), "\n") Total states: 196
cat(" Total zones:", nrow(dynamic_wdpa_full), "\n") Total zones: 216
cat(" PAs with amendments:", length(wdpaids_with_amendments), "\n") PAs with amendments: 68
cat(" PAs baseline only:", length(wdpaids_without_amendments), "\n") PAs baseline only: 100
dynamic_wdpa_full |>
st_drop_geometry() |>
count(amendment_source) |>
print()# A tibble: 32 × 2
amendment_source n
<chr> <int>
1 MDG-10634-2015-boundary_modification-001 1
2 MDG-1299-2015-boundary_modification-001 1
3 MDG-166880-NODATE-secondary_zoning-001 2
4 MDG-2303-2015-boundary_modification-001 2
5 MDG-2307-2015-boundary_modification-001 2
6 MDG-2311-2011-boundary_modification-001 1
7 MDG-2314-2015-boundary_modification-001 1
8 MDG-303700-2015-boundary_modification-001 1
9 MDG-303702-2011-boundary_modification-001 1
10 MDG-352251-NODATE-secondary_zoning-001 2
# ℹ 22 more rows
Le résultat est un objet sf. Les utilisateurs peuvent l’exporter selon leurs besoins :
# Save as RDS (preserves R structure)
saveRDS(dynamic_wdpa_full, "data/dynamic_wdpa.rds")
# Export as GeoPackage (interoperable)
st_write(dynamic_wdpa_full, "data/dynamic_wdpa.gpkg", delete_dsn = TRUE)Deleting source `data/dynamic_wdpa.gpkg' using driver `GPKG'
Writing layer `dynamic_wdpa' to data source
`data/dynamic_wdpa.gpkg' using driver `GPKG'
Writing 216 features with 34 fields and geometry type Unknown (any).
# Export as Parquet (efficient for analysis)
# Note: Convert to regular tibble with WKT geometry for Parquet compatibility
dynamic_wdpa_full |>
mutate(geometry_wkt = st_as_text(geometry)) |>
st_drop_geometry() |>
arrow::write_parquet("data/dynamic_wdpa.parquet")
cat("Exported to:\n")Exported to:
cat(" - data/dynamic_wdpa.rds\n") - data/dynamic_wdpa.rds
cat(" - data/dynamic_wdpa.gpkg\n") - data/dynamic_wdpa.gpkg
cat(" - data/dynamic_wdpa.parquet (with WKT geometry)\n") - data/dynamic_wdpa.parquet (with WKT geometry)
Affichage des chronologies pour quelques cas intéressants :
# Status change example
print_timeline(1299, dynamic_wdpa_full) # Ankarafantsika (RNI to PN)
═══════════════════════════════════════════════════════════════
TIMELINE: Ankarafantsika (WDPAID: 1299 )
═══════════════════════════════════════════════════════════════
┌─ 1927-12-31 to 2002-08-07
│ Status: Réserve Naturelle Intégrale ( Ia )
│ STATUS_YR: 1927
│ ├─ Zone: external_boundary
│ │ Source: WDPA
┌─ 2002-08-07 to 2015-04-21
│ Status: Parc National ( II )
│ STATUS_YR: 2002
│ ├─ Zone: external_boundary
│ │ Source: MDG-1299-2015-boundary_modification-001
┌─ 2015-04-21 to present
│ Status: Parc National ( II )
│ STATUS_YR: 2002
│ ├─ Zone: external_boundary
│ │ Source: WDPA
└─────────────────────────────────────────────────────────────
# Boundary modification example
print_timeline(10634, dynamic_wdpa_full) # Beza Mahafaly
═══════════════════════════════════════════════════════════════
TIMELINE: Beza Mahafaly (WDPAID: 10634 )
═══════════════════════════════════════════════════════════════
┌─ 1986-06-04 to 2015-04-21
│ Status: Reserve Speciale ( IV )
│ STATUS_YR: 1986
│ ├─ Zone: external_boundary
│ │ Source: MDG-10634-2015-boundary_modification-001
┌─ 2015-04-21 to present
│ Status: Reserve Speciale ( IV )
│ STATUS_YR: 1986
│ ├─ Zone: external_boundary
│ │ Source: WDPA
└─────────────────────────────────────────────────────────────
# Complex case (multiple amendments)
print_timeline(2307, dynamic_wdpa_full) # Zahamena
═══════════════════════════════════════════════════════════════
TIMELINE: Tsimanampesotse (WDPAID: 2307 )
═══════════════════════════════════════════════════════════════
┌─ 1927-12-31 to 2002-08-07
│ Status: Réserve Naturelle Intégrale ( Ia )
│ STATUS_YR: 1927
│ ├─ Zone: external_boundary
│ │ Source: MDG-2307-2015-boundary_modification-001
┌─ 2002-08-07 to 2015-04-21
│ Status: Parc National ( II )
│ STATUS_YR: 2002
│ ├─ Zone: external_boundary
│ │ Source: MDG-2307-2015-boundary_modification-001
┌─ 2015-04-21 to present
│ Status: Parc National ( II )
│ STATUS_YR: 2002
│ ├─ Zone: external_boundary
│ │ Source: WDPA
└─────────────────────────────────────────────────────────────