edgarodriguez commited on
Commit
5bff32f
·
verified ·
1 Parent(s): 7531301

Upload depth_interactive_map_alpha.R

Browse files
Files changed (1) hide show
  1. depth_interactive_map_alpha.R +151 -62
depth_interactive_map_alpha.R CHANGED
@@ -204,15 +204,22 @@ FOSAS_COLS <- list(
204
  "cuerpos" = list(col = "cuerpos_total", breaks = c(1, 10, 30, 80, 200))
205
  )
206
 
207
- # CPV 2020 population sub-types -- per grid cell (codigo + nivel).
208
- # Breaks chosen on observed niv=4..8 ranges; map at any nivel is readable.
 
 
 
 
209
  POP_COLS <- list(
210
  "POBTOT" = list(col = "pobtot", label = "Total population",
211
- breaks = c(100, 1000, 5000, 25000, 100000, 500000)),
 
212
  "PNACOP" = list(col = "pnacop", label = "Born abroad",
213
- breaks = c(5, 25, 100, 500, 2500, 10000)),
 
214
  "PRESOP2015" = list(col = "presop2015", label = "Resident abroad (2015)",
215
- breaks = c(2, 10, 50, 250, 1000, 5000))
 
216
  )
217
 
218
  # PATP sub-types (kind controls paint family: "num" = step, "cat" = match).
@@ -374,6 +381,28 @@ WEB_SQL_TEMPLATES <- list(
374
  FROM dim_pam_mun GROUP BY CVEGEO) p
375
  ON c.CVEGEO = p.CVEGEO",
376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  # patp: dim_patp_mun stores numeric values as VARCHAR; TRY_CAST converts
378
  # the 'n.d.' sentinels (and other non-numeric strings) to NULL.
379
  src_patp =
@@ -549,7 +578,10 @@ web_sql_qbreaks <- function(tbl, col, n_colors, filter_zero = TRUE) {
549
  # grid_crosswalk has codigo_n4..codigo_n8, so the equi-join always applies.
550
  # simplify_tol: degrees of simplification applied to g.wkt at query time
551
  # (0 = no simplification; default 0.005 deg ~= 500 m, suitable for N4-N6).
552
- web_sql_grid_unified <- function(niv, has_fosas, limit = NULL, simplify_tol = 0.005) {
 
 
 
553
  col_n <- paste0("codigo_n", niv)
554
  limit_clause <- if (!is.null(limit)) paste0("\nLIMIT ", limit) else ""
555
  wkt_expr <- if (simplify_tol > 0)
@@ -562,6 +594,40 @@ web_sql_grid_unified <- function(niv, has_fosas, limit = NULL, simplify_tol = 0.
562
  fosas_join <- if (has_fosas)
563
  "\n LEFT JOIN dim_fosas_mun f ON g.CVEGEO = f.CVEGEO"
564
  else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  # Subqueries ensure exactly one row per CVEGEO; direct joins on multi-row
566
  # tables (dim_desap_mun ~58k rows, dim_pam_mun ~7.5k rows) explode memory.
567
  desap_sub <- "(SELECT CVEGEO, AVG(final_desap_nl_per_100k) AS desap_rate FROM dim_desap_mun GROUP BY CVEGEO)"
@@ -608,26 +674,14 @@ web_sql_grid_unified <- function(niv, has_fosas, limit = NULL, simplify_tol = 0.
608
  CAST(c.secuestro AS DOUBLE) AS secuestro,
609
  CAST(c.trafico_menores AS DOUBLE) AS trafico_menores,
610
  CAST(c.trata_personas AS DOUBLE) AS trata_personas,
611
- -- CPV 2020 per-cell population (joined by codigo + nivel); -6 is
612
- -- INEGI's confidentiality suppression marker, normalised to NULL.
613
- CASE WHEN cpv.pobtot < 0 THEN NULL ELSE CAST(cpv.pobtot AS DOUBLE) END AS pobtot,
614
- CASE WHEN cpv.pnacop < 0 THEN NULL ELSE CAST(cpv.pnacop AS DOUBLE) END AS pnacop,
615
- CASE WHEN cpv.presop2015 < 0 THEN NULL ELSE CAST(cpv.presop2015 AS DOUBLE) END AS presop2015,
616
  COALESCE(CAST(d.desap_rate AS DOUBLE), 0) AS desap_per_100k{fosas_cols},
617
  COALESCE(r.giim_mun, 'No aplica') AS giim_mun,
618
  COALESCE(CAST(p.pam_total AS INTEGER), 0) AS pam_n,
619
  CAST(s.perception_insecurity_perc AS DOUBLE) AS insecurity_perc,
620
  CAST(s.dark_figures_perc AS DOUBLE) AS dark_figures_perc,
621
- -- patp_mun (numeric-as-VARCHAR -> TRY_CAST; categorical kept as string)
622
- TRY_CAST(patp.t_part AS DOUBLE) AS t_part,
623
- TRY_CAST(patp.t_ocup AS DOUBLE) AS t_ocup,
624
- TRY_CAST(patp.pr AS DOUBLE) AS pr,
625
- CAST(patp.ind_conec AS DOUBLE) AS ind_conec,
626
- patp.pr_pred_loc_rur AS pr_pred_loc_rur,
627
- patp.delitos_fc_b_j_a AS delitos_fc_b_j_a,
628
- -- pobreza_mun (CONEVAL 2020)
629
- CAST(pob.pobreza_pct AS DOUBLE) AS pobreza_pct,
630
- CAST(pob.pobreza_ext_pct AS DOUBLE) AS pobreza_ext_pct,
631
  COALESCE(pts.migrants_n, 0) AS migrants_n,
632
  COALESCE(pts.graves_n, 0) AS graves_n,
633
  COALESCE(pts.ocved_n, 0) AS ocved_n,
@@ -638,10 +692,7 @@ web_sql_grid_unified <- function(niv, has_fosas, limit = NULL, simplify_tol = 0.
638
  LEFT JOIN {desap_sub} d ON g.CVEGEO = d.CVEGEO{fosas_join}
639
  LEFT JOIN dim_remesas_mun r ON g.CVEGEO = r.CVEGEO
640
  LEFT JOIN {pam_sub} p ON g.CVEGEO = p.CVEGEO
641
- LEFT JOIN state_polygons s ON s.CVE_ENT = SUBSTR(g.CVEGEO, 1, 2)
642
- LEFT JOIN dim_pop_cpv_grid cpv ON cpv.codigo = g.codigo AND cpv.nivel = {niv}
643
- LEFT JOIN dim_patp_mun patp ON patp.CVEGEO = g.CVEGEO
644
- LEFT JOIN dim_pobreza_mun pob ON pob.CVEGEO = g.CVEGEO
645
  LEFT JOIN pts_src pts ON g.codigo = pts.codigo{limit_clause}"
646
  )
647
  }
@@ -859,11 +910,12 @@ WEB_SPEC <- list(
859
  list(id = "pam-choro", type = "fill", src = "src_pam",
860
  geom = "polygon", color = "pam_qstep", value = "pam_n",
861
  opacity = 0.7, visible = FALSE),
862
- # pop-choro is GRID-ONLY: data lives in dim_pop_cpv_grid keyed by (codigo,
863
- # nivel). The fill is painted directly on grid cells via add_typed_grid_layers,
864
- # there is no mun-level fallback layer.
865
- list(id = "pop-choro", type = "fill", src = NULL,
866
- geom = "grid_only", color = "pop_qstep", value = "pobtot",
 
867
  opacity = 0.7, visible = FALSE),
868
  # patp-choro and pobreza-choro are also GRID-ONLY (joined into grid_unified)
869
  list(id = "patp-choro", type = "fill", src = NULL,
@@ -957,8 +1009,8 @@ WEB_SPEC <- list(
957
  tooltip = "Railway lines used by migrants, including freight routes known as 'La Bestia'.",
958
  legend = list(type = "line", color = "#9E2A3A", label = "Railway", dashed = TRUE)),
959
  list(kind = "layer", value = "pop-choro",
960
- label = "Population per grid cell (CPV 2020)",
961
- tooltip = "Census 2020 population counts joined to each rHEALPix grid cell at the active nivel. Enable the grid view to see this layer. Select sub-type: total population, born abroad, or resident abroad in 2015.",
962
  legend = list(type = "ramp", pal = "pop", unit = " people")),
963
  list(kind = "radios", group = "pop")
964
  )),
@@ -2536,10 +2588,21 @@ server <- function(input, output, session) {
2536
  if (!is.null(con)) DBI::dbDisconnect(con, shutdown = TRUE)
2537
  })
2538
 
2539
- # Check fosas table once at session start — gates all fosas SQL and JS calls
2540
- has_fosas_tbl <- if (!is.null(con)) {
2541
- tryCatch(DBI::dbExistsTable(con, "dim_fosas_mun"), error = function(e) FALSE)
2542
- } else FALSE
 
 
 
 
 
 
 
 
 
 
 
2543
 
2544
  # Tracks whether grid N7 layers were successfully added to the map
2545
  is_3d <- reactiveVal(FALSE)
@@ -2664,12 +2727,13 @@ server <- function(input, output, session) {
2664
  count_step_expr("pam_n", breaks, PAM_PAL)
2665
  }
2666
 
2667
- # CPV 2020 per-cell population. The sub-type (POBTOT / PNACOP / PRESOP2015)
2668
- # determines which column + break set drives the step paint. NULL cells
2669
- # (INEGI confidentiality suppression) fall to the lightest palette stop.
2670
- pop_grid_expr <- function(pop_type = "POBTOT") {
2671
- info <- POP_COLS[[pop_type]] %||% POP_COLS[["POBTOT"]]
2672
- step_expr(info$col, info$breaks, POP_PAL)
 
2673
  }
2674
 
2675
  # Envipe on the GRID: each grid cell carries its containing state's
@@ -2737,8 +2801,8 @@ server <- function(input, output, session) {
2737
  "desap-choro" = isTRUE(lazy_added$desap),
2738
  "remesas-choro" = isTRUE(lazy_added$remesas),
2739
  "pam-choro" = isTRUE(lazy_added$pam),
2740
- # pop / patp / pobreza are grid-only -- no mun-level layer to gate.
2741
- "pop-choro" = FALSE,
2742
  "patp-choro" = FALSE,
2743
  "pobreza-choro" = FALSE,
2744
  "fosas-choro" = isTRUE(lazy_added$fosas),
@@ -2790,6 +2854,15 @@ server <- function(input, output, session) {
2790
  )
2791
  }
2792
 
 
 
 
 
 
 
 
 
 
2793
  for (lid in NON_GRID_LAYERS) {
2794
  if (!layer_ready(lid)) next
2795
  prx <- prx |> set_layout_property(
@@ -3114,7 +3187,10 @@ server <- function(input, output, session) {
3114
  nv_tol <- if (niv >= 7L) 0.002 else 0.005
3115
  tryCatch({
3116
  DBI::dbGetQuery(con,
3117
- web_sql_grid_unified(niv, has_fosas_tbl, limit, simplify_tol = nv_tol)) |>
 
 
 
3118
  sf::st_as_sf(wkt = "wkt", crs = 4326) |>
3119
  dplyr::select(-wkt)
3120
  }, error = function(e) { message("grid_nivel", niv, ": ", e$message); NULL })
@@ -3318,7 +3394,11 @@ server <- function(input, output, session) {
3318
  observeEvent(input$grid_nivel, {
3319
  req(input$grid_nivel == "7", !grid7_loaded(), !is.null(con))
3320
  grid7_sf <<- tryCatch({
3321
- DBI::dbGetQuery(con, web_sql_grid_unified(7, has_fosas_tbl, simplify_tol = 0.002)) |>
 
 
 
 
3322
  sf::st_as_sf(wkt = "wkt", crs = 4326) |>
3323
  dplyr::select(-wkt)
3324
  }, error = function(e) { message("N7 lazy load: ", e$message); NULL })
@@ -3387,12 +3467,21 @@ server <- function(input, output, session) {
3387
  nv_tol <- if (nv_chr == "7") 0.002 else 0.005
3388
  sql <- if (has_pre) paste("SELECT * FROM", pre_tbl)
3389
  else web_sql_grid_unified(as.integer(nv_chr), has_fosas_tbl,
3390
- simplify_tol = nv_tol)
 
 
 
3391
  sf_data <- tryCatch({
3392
  DBI::dbGetQuery(con, sql) |>
3393
  sf::st_as_sf(wkt = "wkt", crs = 4326) |>
3394
  dplyr::select(-wkt)
3395
- }, error = function(e) { message("lazy grid n", nv_chr, ": ", e$message); NULL })
 
 
 
 
 
 
3396
  if (!is.null(sf_data)) {
3397
  maplibre_proxy("map") |> add_typed_grid_layers(sf_data, nv_chr)
3398
  lazy_added[[key]] <<- TRUE
@@ -3427,7 +3516,14 @@ server <- function(input, output, session) {
3427
  fill_color = pam_qstep_expr(), fill_opacity = 0.7,
3428
  tooltip = list("concat", "<b>", list("get","municipio"), "</b>",
3429
  "<br>Irregular migration events: ", list("to-string", list("get","pam_n")))))
3430
- # pop-choro is GRID-ONLY (CPV 2020 per cell). No mun-level fallback.
 
 
 
 
 
 
 
3431
  if (!grid_on && "fosas-choro" %in% active && has_fosas_tbl)
3432
  lazy_add_fill("fosas", "src_fosas", "fosas-choro", function(prx, d)
3433
  prx |> add_fill_layer("fosas-choro", source = d,
@@ -3495,19 +3591,12 @@ server <- function(input, output, session) {
3495
  )
3496
  updateSelectInput(session, "state_select",
3497
  choices = c("Choose a state..." = "", state_ch))
3498
- # Pre-materialise grid joins for N4/N5/N6 into session TEMP TABLEs.
3499
- # Runs after national totals so the sidebar populates first.
3500
- # lazy_add_grid() will use these tables instead of re-running the full CTE.
3501
- for (.nv in c("4", "5", "6")) {
3502
- nv_tol <- 0.005
3503
- tryCatch(
3504
- DBI::dbExecute(con, paste0(
3505
- "CREATE TEMP TABLE IF NOT EXISTS grid_pre_n", .nv, " AS ",
3506
- web_sql_grid_unified(as.integer(.nv), has_fosas_tbl, simplify_tol = nv_tol)
3507
- )),
3508
- error = function(e) message("grid_pre_n", .nv, ": ", e$message)
3509
- )
3510
- }
3511
  }, once = TRUE)
3512
 
3513
  # Grid overlay on/off is driven explicitly by resolution clicks in the menu
 
204
  "cuerpos" = list(col = "cuerpos_total", breaks = c(1, 10, 30, 80, 200))
205
  )
206
 
207
+ # CPV 2020 population sub-types. The same column name (e.g. pobtot) lives in
208
+ # both the per-cell grid join AND the muni-level aggregation, so step_expr can
209
+ # read either; only the BREAKS differ between modes.
210
+ # breaks_grid -> niv 4..7 cells (chosen on observed CPV ranges per nivel)
211
+ # breaks_mun -> per-municipality totals (CDMX/Monterrey/Guadalajara reach
212
+ # the millions; smallest munis a few hundred)
213
  POP_COLS <- list(
214
  "POBTOT" = list(col = "pobtot", label = "Total population",
215
+ breaks_grid = c(100, 1000, 5000, 25000, 100000, 500000),
216
+ breaks_mun = c(5000, 25000, 100000, 250000, 500000, 1000000)),
217
  "PNACOP" = list(col = "pnacop", label = "Born abroad",
218
+ breaks_grid = c(5, 25, 100, 500, 2500, 10000),
219
+ breaks_mun = c(25, 100, 500, 2500, 10000, 50000)),
220
  "PRESOP2015" = list(col = "presop2015", label = "Resident abroad (2015)",
221
+ breaks_grid = c(2, 10, 50, 250, 1000, 5000),
222
+ breaks_mun = c(10, 50, 250, 1000, 5000, 25000))
223
  )
224
 
225
  # PATP sub-types (kind controls paint family: "num" = step, "cat" = match).
 
381
  FROM dim_pam_mun GROUP BY CVEGEO) p
382
  ON c.CVEGEO = p.CVEGEO",
383
 
384
+ # Municipal-level CPV 2020 totals. Aggregates at nivel 8 (cell ~1.3km cell
385
+ # length, ~224k cells) so even the smallest CDMX delegaciones are larger than
386
+ # any cell and don't get inflated by single-cell over-attribution.
387
+ src_pop_mun =
388
+ "WITH agg AS (
389
+ SELECT g.CVEGEO,
390
+ SUM(CASE WHEN cpv.pobtot >= 0 THEN cpv.pobtot ELSE 0 END) AS pobtot,
391
+ SUM(CASE WHEN cpv.pnacop >= 0 THEN cpv.pnacop ELSE 0 END) AS pnacop,
392
+ SUM(CASE WHEN cpv.presop2015 >= 0 THEN cpv.presop2015 ELSE 0 END) AS presop2015
393
+ FROM grid_nivel8 g
394
+ LEFT JOIN dim_pop_cpv_grid cpv
395
+ ON cpv.codigo = g.codigo AND cpv.nivel = 8
396
+ GROUP BY g.CVEGEO
397
+ )
398
+ SELECT c.municipio, c.entidad, c.CVEGEO,
399
+ COALESCE(a.pobtot, 0) AS pobtot,
400
+ COALESCE(a.pnacop, 0) AS pnacop,
401
+ COALESCE(a.presop2015, 0) AS presop2015,
402
+ c.wkt
403
+ FROM crime_mun c
404
+ LEFT JOIN agg a ON a.CVEGEO = c.CVEGEO",
405
+
406
  # patp: dim_patp_mun stores numeric values as VARCHAR; TRY_CAST converts
407
  # the 'n.d.' sentinels (and other non-numeric strings) to NULL.
408
  src_patp =
 
578
  # grid_crosswalk has codigo_n4..codigo_n8, so the equi-join always applies.
579
  # simplify_tol: degrees of simplification applied to g.wkt at query time
580
  # (0 = no simplification; default 0.005 deg ~= 500 m, suitable for N4-N6).
581
+ web_sql_grid_unified <- function(niv, has_fosas, limit = NULL, simplify_tol = 0.005,
582
+ has_cpv_grid = TRUE,
583
+ has_patp = TRUE,
584
+ has_pobreza = TRUE) {
585
  col_n <- paste0("codigo_n", niv)
586
  limit_clause <- if (!is.null(limit)) paste0("\nLIMIT ", limit) else ""
587
  wkt_expr <- if (simplify_tol > 0)
 
594
  fosas_join <- if (has_fosas)
595
  "\n LEFT JOIN dim_fosas_mun f ON g.CVEGEO = f.CVEGEO"
596
  else ""
597
+ # Each optional extension is rendered as (columns + join) pairs. If the
598
+ # backing table is missing on the deployed DB, we substitute NULLs for the
599
+ # columns and skip the join entirely -- the rest of the grid still loads.
600
+ cpv_cols <- if (has_cpv_grid)
601
+ "CASE WHEN cpv.pobtot < 0 THEN NULL ELSE CAST(cpv.pobtot AS DOUBLE) END AS pobtot,
602
+ CASE WHEN cpv.pnacop < 0 THEN NULL ELSE CAST(cpv.pnacop AS DOUBLE) END AS pnacop,
603
+ CASE WHEN cpv.presop2015 < 0 THEN NULL ELSE CAST(cpv.presop2015 AS DOUBLE) END AS presop2015"
604
+ else
605
+ "CAST(NULL AS DOUBLE) AS pobtot, CAST(NULL AS DOUBLE) AS pnacop, CAST(NULL AS DOUBLE) AS presop2015"
606
+ cpv_join <- if (has_cpv_grid)
607
+ paste0("\n LEFT JOIN dim_pop_cpv_grid cpv ON cpv.codigo = g.codigo AND cpv.nivel = ", niv)
608
+ else ""
609
+ patp_cols <- if (has_patp)
610
+ "TRY_CAST(patp.t_part AS DOUBLE) AS t_part,
611
+ TRY_CAST(patp.t_ocup AS DOUBLE) AS t_ocup,
612
+ TRY_CAST(patp.pr AS DOUBLE) AS pr,
613
+ CAST(patp.ind_conec AS DOUBLE) AS ind_conec,
614
+ patp.pr_pred_loc_rur AS pr_pred_loc_rur,
615
+ patp.delitos_fc_b_j_a AS delitos_fc_b_j_a"
616
+ else
617
+ "CAST(NULL AS DOUBLE) AS t_part, CAST(NULL AS DOUBLE) AS t_ocup, CAST(NULL AS DOUBLE) AS pr,
618
+ CAST(NULL AS DOUBLE) AS ind_conec, CAST(NULL AS VARCHAR) AS pr_pred_loc_rur,
619
+ CAST(NULL AS VARCHAR) AS delitos_fc_b_j_a"
620
+ patp_join <- if (has_patp)
621
+ "\n LEFT JOIN dim_patp_mun patp ON patp.CVEGEO = g.CVEGEO"
622
+ else ""
623
+ pob_cols <- if (has_pobreza)
624
+ "CAST(pob.pobreza_pct AS DOUBLE) AS pobreza_pct,
625
+ CAST(pob.pobreza_ext_pct AS DOUBLE) AS pobreza_ext_pct"
626
+ else
627
+ "CAST(NULL AS DOUBLE) AS pobreza_pct, CAST(NULL AS DOUBLE) AS pobreza_ext_pct"
628
+ pob_join <- if (has_pobreza)
629
+ "\n LEFT JOIN dim_pobreza_mun pob ON pob.CVEGEO = g.CVEGEO"
630
+ else ""
631
  # Subqueries ensure exactly one row per CVEGEO; direct joins on multi-row
632
  # tables (dim_desap_mun ~58k rows, dim_pam_mun ~7.5k rows) explode memory.
633
  desap_sub <- "(SELECT CVEGEO, AVG(final_desap_nl_per_100k) AS desap_rate FROM dim_desap_mun GROUP BY CVEGEO)"
 
674
  CAST(c.secuestro AS DOUBLE) AS secuestro,
675
  CAST(c.trafico_menores AS DOUBLE) AS trafico_menores,
676
  CAST(c.trata_personas AS DOUBLE) AS trata_personas,
677
+ {cpv_cols},
 
 
 
 
678
  COALESCE(CAST(d.desap_rate AS DOUBLE), 0) AS desap_per_100k{fosas_cols},
679
  COALESCE(r.giim_mun, 'No aplica') AS giim_mun,
680
  COALESCE(CAST(p.pam_total AS INTEGER), 0) AS pam_n,
681
  CAST(s.perception_insecurity_perc AS DOUBLE) AS insecurity_perc,
682
  CAST(s.dark_figures_perc AS DOUBLE) AS dark_figures_perc,
683
+ {patp_cols},
684
+ {pob_cols},
 
 
 
 
 
 
 
 
685
  COALESCE(pts.migrants_n, 0) AS migrants_n,
686
  COALESCE(pts.graves_n, 0) AS graves_n,
687
  COALESCE(pts.ocved_n, 0) AS ocved_n,
 
692
  LEFT JOIN {desap_sub} d ON g.CVEGEO = d.CVEGEO{fosas_join}
693
  LEFT JOIN dim_remesas_mun r ON g.CVEGEO = r.CVEGEO
694
  LEFT JOIN {pam_sub} p ON g.CVEGEO = p.CVEGEO
695
+ LEFT JOIN state_polygons s ON s.CVE_ENT = SUBSTR(g.CVEGEO, 1, 2){cpv_join}{patp_join}{pob_join}
 
 
 
696
  LEFT JOIN pts_src pts ON g.codigo = pts.codigo{limit_clause}"
697
  )
698
  }
 
910
  list(id = "pam-choro", type = "fill", src = "src_pam",
911
  geom = "polygon", color = "pam_qstep", value = "pam_n",
912
  opacity = 0.7, visible = FALSE),
913
+ # pop-choro paints in BOTH modes:
914
+ # non-grid: per-municipality aggregate of CPV cells (src_pop_mun)
915
+ # grid: per-cell value at the active nivel (dim_pop_cpv_grid, joined
916
+ # into web_sql_grid_unified)
917
+ list(id = "pop-choro", type = "fill", src = "src_pop_mun",
918
+ geom = "polygon", color = "pop_qstep", value = "pobtot",
919
  opacity = 0.7, visible = FALSE),
920
  # patp-choro and pobreza-choro are also GRID-ONLY (joined into grid_unified)
921
  list(id = "patp-choro", type = "fill", src = NULL,
 
1009
  tooltip = "Railway lines used by migrants, including freight routes known as 'La Bestia'.",
1010
  legend = list(type = "line", color = "#9E2A3A", label = "Railway", dashed = TRUE)),
1011
  list(kind = "layer", value = "pop-choro",
1012
+ label = "Population (CPV 2020)",
1013
+ tooltip = "Census 2020 population counts. With the grid view off, each municipality shows the sum of its CPV cells (3 sub-types: total, born abroad, resident abroad in 2015). With the grid view on, each rHEALPix cell shows its own value at the active nivel (n4-n7).",
1014
  legend = list(type = "ramp", pal = "pop", unit = " people")),
1015
  list(kind = "radios", group = "pop")
1016
  )),
 
2588
  if (!is.null(con)) DBI::dbDisconnect(con, shutdown = TRUE)
2589
  })
2590
 
2591
+ # Check optional tables once at session start. Each gates the SQL builders
2592
+ # so a missing table does not cascade into the rest of the grid_unified
2593
+ # query (which would otherwise blank all grid layers).
2594
+ tbl_exists <- function(t) {
2595
+ if (is.null(con)) return(FALSE)
2596
+ tryCatch(DBI::dbExistsTable(con, t), error = function(e) FALSE)
2597
+ }
2598
+ has_fosas_tbl <- tbl_exists("dim_fosas_mun")
2599
+ has_cpv_grid_tbl <- tbl_exists("dim_pop_cpv_grid")
2600
+ has_patp_tbl <- tbl_exists("dim_patp_mun")
2601
+ has_pobreza_tbl <- tbl_exists("dim_pobreza_mun")
2602
+ message("[deploy-check] dim_pop_cpv_grid=", has_cpv_grid_tbl,
2603
+ " dim_patp_mun=", has_patp_tbl,
2604
+ " dim_pobreza_mun=", has_pobreza_tbl,
2605
+ " dim_fosas_mun=", has_fosas_tbl)
2606
 
2607
  # Tracks whether grid N7 layers were successfully added to the map
2608
  is_3d <- reactiveVal(FALSE)
 
2727
  count_step_expr("pam_n", breaks, PAM_PAL)
2728
  }
2729
 
2730
+ # CPV 2020 population paint. mode = "grid" reads per-cell values (joined into
2731
+ # web_sql_grid_unified); mode = "mun" reads per-municipality aggregates
2732
+ # (src_pop_mun). Same column name in both modes; only the breaks differ.
2733
+ pop_grid_expr <- function(pop_type = "POBTOT", mode = "grid") {
2734
+ info <- POP_COLS[[pop_type]] %||% POP_COLS[["POBTOT"]]
2735
+ breaks <- if (identical(mode, "mun")) info$breaks_mun else info$breaks_grid
2736
+ step_expr(info$col, breaks, POP_PAL)
2737
  }
2738
 
2739
  # Envipe on the GRID: each grid cell carries its containing state's
 
2801
  "desap-choro" = isTRUE(lazy_added$desap),
2802
  "remesas-choro" = isTRUE(lazy_added$remesas),
2803
  "pam-choro" = isTRUE(lazy_added$pam),
2804
+ "pop-choro" = isTRUE(lazy_added$pop),
2805
+ # patp / pobreza remain grid-only -- no mun-level layer to gate.
2806
  "patp-choro" = FALSE,
2807
  "pobreza-choro" = FALSE,
2808
  "fosas-choro" = isTRUE(lazy_added$fosas),
 
2854
  )
2855
  }
2856
 
2857
+ # Sync pop fill color to selected sub-type (POBTOT / PNACOP / PRESOP2015)
2858
+ # when grid is OFF. Uses mun-mode breaks via pop_grid_expr(mode = "mun").
2859
+ if (!grid_on && "pop-choro" %in% active && layer_ready("pop-choro")) {
2860
+ prx <- prx |> set_paint_property(
2861
+ "pop-choro", "fill-color",
2862
+ pop_grid_expr(pop_type, "mun")
2863
+ )
2864
+ }
2865
+
2866
  for (lid in NON_GRID_LAYERS) {
2867
  if (!layer_ready(lid)) next
2868
  prx <- prx |> set_layout_property(
 
3187
  nv_tol <- if (niv >= 7L) 0.002 else 0.005
3188
  tryCatch({
3189
  DBI::dbGetQuery(con,
3190
+ web_sql_grid_unified(niv, has_fosas_tbl, limit, simplify_tol = nv_tol,
3191
+ has_cpv_grid = has_cpv_grid_tbl,
3192
+ has_patp = has_patp_tbl,
3193
+ has_pobreza = has_pobreza_tbl)) |>
3194
  sf::st_as_sf(wkt = "wkt", crs = 4326) |>
3195
  dplyr::select(-wkt)
3196
  }, error = function(e) { message("grid_nivel", niv, ": ", e$message); NULL })
 
3394
  observeEvent(input$grid_nivel, {
3395
  req(input$grid_nivel == "7", !grid7_loaded(), !is.null(con))
3396
  grid7_sf <<- tryCatch({
3397
+ DBI::dbGetQuery(con,
3398
+ web_sql_grid_unified(7, has_fosas_tbl, simplify_tol = 0.002,
3399
+ has_cpv_grid = has_cpv_grid_tbl,
3400
+ has_patp = has_patp_tbl,
3401
+ has_pobreza = has_pobreza_tbl)) |>
3402
  sf::st_as_sf(wkt = "wkt", crs = 4326) |>
3403
  dplyr::select(-wkt)
3404
  }, error = function(e) { message("N7 lazy load: ", e$message); NULL })
 
3467
  nv_tol <- if (nv_chr == "7") 0.002 else 0.005
3468
  sql <- if (has_pre) paste("SELECT * FROM", pre_tbl)
3469
  else web_sql_grid_unified(as.integer(nv_chr), has_fosas_tbl,
3470
+ simplify_tol = nv_tol,
3471
+ has_cpv_grid = has_cpv_grid_tbl,
3472
+ has_patp = has_patp_tbl,
3473
+ has_pobreza = has_pobreza_tbl)
3474
  sf_data <- tryCatch({
3475
  DBI::dbGetQuery(con, sql) |>
3476
  sf::st_as_sf(wkt = "wkt", crs = 4326) |>
3477
  dplyr::select(-wkt)
3478
+ }, error = function(e) {
3479
+ message("lazy grid n", nv_chr, ": ", e$message)
3480
+ try(showNotification(paste0("Grid n", nv_chr, " load failed: ",
3481
+ conditionMessage(e)),
3482
+ type = "error", duration = 12), silent = TRUE)
3483
+ NULL
3484
+ })
3485
  if (!is.null(sf_data)) {
3486
  maplibre_proxy("map") |> add_typed_grid_layers(sf_data, nv_chr)
3487
  lazy_added[[key]] <<- TRUE
 
3516
  fill_color = pam_qstep_expr(), fill_opacity = 0.7,
3517
  tooltip = list("concat", "<b>", list("get","municipio"), "</b>",
3518
  "<br>Irregular migration events: ", list("to-string", list("get","pam_n")))))
3519
+ if (!grid_on && "pop-choro" %in% active && has_cpv_grid_tbl)
3520
+ lazy_add_fill("pop", "src_pop_mun", "pop-choro", function(prx, d)
3521
+ prx |> add_fill_layer("pop-choro", source = d,
3522
+ fill_color = pop_grid_expr("POBTOT", "mun"), fill_opacity = 0.7,
3523
+ tooltip = list("concat", "<b>", list("get","municipio"), "</b>",
3524
+ "<br>Total pop (CPV): ", list("to-string", list("get","pobtot")),
3525
+ "<br>Born abroad: ", list("to-string", list("get","pnacop")),
3526
+ "<br>Resident abroad 2015: ", list("to-string", list("get","presop2015")))))
3527
  if (!grid_on && "fosas-choro" %in% active && has_fosas_tbl)
3528
  lazy_add_fill("fosas", "src_fosas", "fosas-choro", function(prx, d)
3529
  prx |> add_fill_layer("fosas-choro", source = d,
 
3591
  )
3592
  updateSelectInput(session, "state_select",
3593
  choices = c("Choose a state..." = "", state_ch))
3594
+ # Pre-materialise grid joins for N4/N5/N6 -- TEMPORARILY DISABLED.
3595
+ # The extended grid_unified CTE was blocking session startup on HF (3
3596
+ # synchronous queries against a query with 9 LEFT JOINs). lazy_add_grid()
3597
+ # now falls back to the direct CTE on first user click, which finishes
3598
+ # within a few seconds and lets the user keep using the rest of the app.
3599
+ # If this proves OK on HF, re-enable with a background/async strategy.
 
 
 
 
 
 
 
3600
  }, once = TRUE)
3601
 
3602
  # Grid overlay on/off is driven explicitly by resolution clicks in the menu