9  CAR - Règles de consolidation et d’audit

9.1 Vue d’ensemble

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 :

  1. WDPA comme base de référence : L’état actuel est hérité par défaut
  2. Les amendements YAML prévalent : L’information datée explicite est prioritaire
  3. Uniquement les deltas: Seules les modifications sont documentées dans les amendements, les information actuellement valides incluses dans WDPA ne sont pas dupliquées.
  4. Segmentation temporelle : Toutes les dates (WDPA et amendements) créent des intervalles non chevauchants
  5. Modèle de zones : Limites externes et zonages secondaires coexistent
  6. Fail-fast : Les conflits génèrent des erreurs plutôt que de se résoudre silencieusement

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 actuelle
  • secondary_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.

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")

9.2 Lecture du registre d’amendements

Le registre d’amendements consiste en fichiers YAML (métadonnées) et fichiers GeoJSON (géométries) créés par 07_curation.qmd.

Voir le code
#' 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:
Voir le code
cat("  Total amendments:", nrow(amendments$all), "\n")
  Total amendments: 106 
Voir le code
cat("  Spatial:", nrow(amendments$spatial), "\n")
  Spatial: 35 
Voir le code
cat("  Attribute:", nrow(amendments$attribute), "\n")
  Attribute: 71 
Voir le code
cat("\nAmendment types:\n")

Amendment types:
Voir le code
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

9.3 Fonctions de consolidation

9.3.1 Génération de chronologie

Création d’intervalles temporels non chevauchants en combinant tous les marqueurs de dates issus de WDPA et des amendements.

Voir le code
#' 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
  )
}

9.3.2 Règles de précédence

Précédence spatiale :

  1. Pour boundary_modification : La géométrie de l’amendement remplace WDPA pendant sa période de validité
  2. Pour secondary_zoning : Additif (crée des zones supplémentaires en plus de la limite)
  3. Plusieurs boundary_modifications dans la même période : ERREUR (conflit)

Précédence des attributs :

  1. Les attributs des amendements remplacent WDPA pendant la période de validité
  2. Les attributs non spécifiés héritent de WDPA
  3. Plusieurs amendements d’attributs avec des attributs chevauchants dans la même période : ERREUR
Voir le code
#' 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
}

9.3.3 Consolidation des états

Application des amendements à la base WDPA pour chaque intervalle temporel, créant des enregistrements de zones.

Voir le code
#' 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
}

9.4 Auditabilité : Affichage de chronologie

Fonction simple pour visualiser l’évolution temporelle d’une AP.

Voir le code
#' 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)
}

9.5 Test : une AP unique

Test de consolidation sur Ankarafantsika (a des changements de statut et modifications de limites).

Voir le code
# 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 
└─────────────────────────────────────────────────────────────
Voir le code
# 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            

9.6 Validation : vérification systématique

Détection des problèmes potentiels dans le registre d’amendements avant consolidation complète.

Voir le code
# 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
Voir le code
# 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 
Voir le code
# 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
Voir le code
# 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

9.7 Génération du jeu de données complet

Application à toutes les AP de Madagascar.

Voir le code
# 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...
Voir le code
# 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:
Voir le code
cat("  Total states:", n_distinct(dynamic_wdpa_full$state_id), "\n")
  Total states: 196 
Voir le code
cat("  Total zones:", nrow(dynamic_wdpa_full), "\n")
  Total zones: 216 
Voir le code
cat("  PAs with amendments:", length(wdpaids_with_amendments), "\n")
  PAs with amendments: 68 
Voir le code
cat("  PAs baseline only:", length(wdpaids_without_amendments), "\n")
  PAs baseline only: 100 
Voir le code
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

9.8 Export

Le résultat est un objet sf. Les utilisateurs peuvent l’exporter selon leurs besoins :

Voir le code
# 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).
Voir le code
# 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:
Voir le code
cat("  - data/dynamic_wdpa.rds\n")
  - data/dynamic_wdpa.rds
Voir le code
cat("  - data/dynamic_wdpa.gpkg\n")
  - data/dynamic_wdpa.gpkg
Voir le code
cat("  - data/dynamic_wdpa.parquet (with WKT geometry)\n")
  - data/dynamic_wdpa.parquet (with WKT geometry)

9.9 Exemples de chronologies

Affichage des chronologies pour quelques cas intéressants :

Voir le code
# 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 
└─────────────────────────────────────────────────────────────
Voir le code
# 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 
└─────────────────────────────────────────────────────────────
Voir le code
# 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 
└─────────────────────────────────────────────────────────────