---
title: "spatialkit Demo — Tessellations & Models on Synthetic North Carolina Data"
output:
  html_document:
    toc: true
    toc_float: true
    theme: flatly
    code_folding: show
    fig_width: 10
    fig_height: 7
vignette: >
  %\VignetteIndexEntry{spatialkit Demo — Tessellations & Models}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  echo       = TRUE,
  message    = FALSE,
  warning    = FALSE,
  fig.align  = "center",
  out.width  = "100%"
)
```

## Overview

This vignette generates **synthetic spatial data** over the state of
North Carolina and walks through the core `spatialkit` workflow:

1. Build four tessellation types (Voronoi, hex, square, Delaunay)
2. Assign observation points to cells and compute cell-level summaries
3. Produce **choropleth maps** showing cell-level mean response
4. Fit a GWR model and visualise residuals across tessellations
5. Run 5-fold cross-validation

Everything is self-contained — the boundary comes from the `nc.shp` demo
shapefile bundled with the `sf` package, so no external files are needed.

---

## 1. Load Packages & Create Boundary

```{r load-packages}
library(spatialkit)
library(sf)
library(dplyr)
library(ggplot2)

set.seed(42)
```

We load the North Carolina county boundaries shipped with `sf`, dissolve them
into a single state outline, and project to **NAD83 / North Carolina (ftUS)**
(EPSG:2264) for proper distance-based tessellations:

```{r boundary}
nc_counties <- st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
nc_boundary <- nc_counties |>
  st_union() |>
  st_transform(2264) |>
  st_as_sf()
```

---

## 2. Generate Synthetic Observation Data

We scatter **300 points** inside the state boundary with two predictors and a
spatially-varying response:

- `elevation` — gradient increasing west → east, plus noise
- `pop_density` — decays with distance from two fake "cities" (Charlotte, Raleigh)
- `y` — response driven by predictors + a spatial sine/cosine trend

```{r fake-data}
n_points <- 300

# Sample points inside the NC boundary
pts_raw <- st_sample(nc_boundary, size = n_points, type = "random")
pts_coords <- st_coordinates(pts_raw)
x_coords <- pts_coords[, 1]
y_coords <- pts_coords[, 2]

# Predictors
elevation <- scale(x_coords)[, 1] * 500 + rnorm(n_points, 3000, 400)

# Approximate projected coords for Charlotte & Raleigh in EPSG:2264
city1 <- c(1530000, 550000)   # Charlotte-ish
city2 <- c(2150000, 750000)   # Raleigh-ish
dist_to_city <- pmin(
  sqrt((x_coords - city1[1])^2 + (y_coords - city1[2])^2),
  sqrt((x_coords - city2[1])^2 + (y_coords - city2[2])^2)
)
pop_density <- exp(-dist_to_city / 400000) * 5000 + rnorm(n_points, 200, 100)
pop_density <- pmax(pop_density, 10)

# Response
y_response <- 50 +
  0.01  * elevation +
  0.005 * pop_density +
  2.0   * sin(x_coords / 300000) * cos(y_coords / 300000) +
  rnorm(n_points, 0, 5)

points_sf <- st_sf(
  y           = y_response,
  elevation   = elevation,
  pop_density = pop_density,
  geometry    = pts_raw
)
```

Quick sanity check — points over boundary:

```{r quick-peek, fig.height=5}
ggplot() +
  geom_sf(data = nc_boundary, fill = "grey95", color = "black") +
  geom_sf(data = points_sf, aes(color = y), size = 1.2) +
  scale_color_viridis_c(name = "Response (y)") +
  theme_void() +
  ggtitle("Raw observation points — North Carolina")
```

---

## 3. Build Four Tessellation Types

### 3a. Voronoi

```{r tess-voronoi}
seeds <- get_voronoi_seeds(
  boundary      = nc_boundary,
  sample_points = points_sf,
  method        = "kmeans",
  n             = 40
)

tess_voronoi <- build_tessellation(
  points_sf, boundary = nc_boundary,
  method = "voronoi", clip = TRUE, quiet = TRUE
)
```

### 3b. Hexagonal Grid (~50 cells)

```{r tess-hex}
tess_hex <- build_tessellation(
  points_sf, boundary = nc_boundary,
  method = "hex", approx_n_cells = 50, clip = TRUE, quiet = TRUE
)
```

### 3c. Square Grid (~50 cells)

```{r tess-square}
tess_square <- build_tessellation(
  points_sf, boundary = nc_boundary,
  method = "square", approx_n_cells = 50, clip = TRUE, quiet = TRUE
)
```

### 3d. Delaunay Triangles

```{r tess-tri}
tess_tri <- tryCatch(
  build_tessellation(
    points_sf, boundary = nc_boundary,
    method = "triangles", clip = TRUE, quiet = TRUE
  ),
  error = function(e) {
    message("Delaunay skipped: ", conditionMessage(e))
    NULL
  }
)
```

### Cell counts

```{r cell-counts}
cat(sprintf(
  "Voronoi: %d | Hex: %d | Square: %d | Triangles: %s\n",
  nrow(tess_voronoi$cells),
  nrow(tess_hex$cells),
  nrow(tess_square$cells),
  if (!is.null(tess_tri)) nrow(tess_tri$cells) else "skipped"
))
```

---

## 4. Choropleth Maps — Cell-Level Mean Response

For each tessellation we assign observation points to cells, compute the mean
response `y` per cell, and render a **filled choropleth** with a clean look.

```{r choropleth-helper}
#' Assign points → cells, compute mean, and produce a clean choropleth
make_choropleth <- function(tess, boundary, points, fill_var = "y",
                            palette = "viridis", title = NULL,
                            legend_title = "Mean Response (y)") {
  cells <- tess$cells

  # Identify the id column
  id_col <- if ("cell_id" %in% names(cells)) "cell_id" else "poly_id"
  if (!id_col %in% names(cells)) {
    cells$cell_id <- seq_len(nrow(cells))
    id_col <- "cell_id"
  }

  # Assign points to cells and compute cell-level mean
  assigned <- assign_features_to_polygons(points, cells, polygon_id_col = id_col)

  cell_summary <- assigned |>
    st_drop_geometry() |>
    group_by(.data[[id_col]]) |>
    summarise(
      fill_value = mean(.data[[fill_var]], na.rm = TRUE),
      n_obs      = n(),
      .groups    = "drop"
    )

  cells <- left_join(cells, cell_summary, by = id_col)

  # Build the choropleth via plot_tessellation_map
  plot_tessellation_map(
    tessellation_sf = cells,
    boundary        = boundary,
    fill_col        = "fill_value",
    palette         = palette,
    tile_alpha      = 0.9,
    outline_col     = "white",
    outline_size    = 0.3,
    boundary_col    = "grey20",
    boundary_size   = 0.8,
    legend_title    = legend_title,
    title           = title,
    subtitle        = sprintf("%d cells  |  %d observations", nrow(cells), nrow(points))
  )
}
```

### 4a. Voronoi Choropleth

```{r choro-voronoi, fig.cap="Voronoi choropleth — mean response per cell"}
make_choropleth(tess_voronoi, nc_boundary, points_sf,
                title = "Voronoi Tessellation — Mean Response")
```

### 4b. Hexagonal Grid Choropleth

```{r choro-hex, fig.cap="Hex grid choropleth — mean response per cell"}
make_choropleth(tess_hex, nc_boundary, points_sf,
                title = "Hexagonal Grid — Mean Response")
```

### 4c. Square Grid Choropleth

```{r choro-square, fig.cap="Square grid choropleth — mean response per cell"}
make_choropleth(tess_square, nc_boundary, points_sf,
                title = "Square Grid — Mean Response")
```

### 4d. Delaunay Choropleth

```{r choro-tri, fig.cap="Delaunay choropleth — mean response per cell", eval=exists("tess_tri") && !is.null(tess_tri)}
make_choropleth(tess_tri, nc_boundary, points_sf,
                title = "Delaunay Triangulation — Mean Response")
```

---

## 5. Side-by-Side Comparison

```{r comparison-panel, fig.width=14, fig.height=6, fig.cap="All tessellations at a glance"}
if (requireNamespace("patchwork", quietly = TRUE)) {
  library(patchwork)

  p1 <- make_choropleth(tess_voronoi, nc_boundary, points_sf,
                         title = "Voronoi")
  p2 <- make_choropleth(tess_hex,     nc_boundary, points_sf,
                         title = "Hex Grid")
  p3 <- make_choropleth(tess_square,  nc_boundary, points_sf,
                         title = "Square Grid")

  (p1 | p2 | p3) +
    plot_annotation(
      title    = "Tessellation Comparison — Cell-Level Mean Response",
      subtitle = sprintf("%d observations, North Carolina", n_points),
      theme    = theme(
        plot.title    = element_text(size = 16, face = "bold"),
        plot.subtitle = element_text(size = 11, color = "grey40")
      )
    )
} else {
  cat("Install 'patchwork' for the side-by-side panel: install.packages('patchwork')")
}
```

---

## 6. GWR Model Fitting

Fit a geographically weighted regression: `y ~ elevation + pop_density`.

```{r gwr-fit}
response_var   <- "y"
predictor_vars <- c("elevation", "pop_density")

gwr_fit <- tryCatch({
  fit_gwr_model(
    data_sf        = points_sf,
    response_var   = response_var,
    predictor_vars = predictor_vars,
    adaptive       = TRUE,
    kernel         = "bisquare"
  )
}, error = function(e) {
  message("GWR skipped: ", conditionMessage(e))
  NULL
})
```

```{r gwr-summary, eval=exists("gwr_fit") && !is.null(gwr_fit)}
cat(sprintf("Bandwidth: %.1f  |  R²: %.3f  |  RMSE: %.3f\n",
            gwr_fit$info$bandwidth,
            gwr_fit$metrics$r_squared,
            gwr_fit$metrics$rmse))
```

### GWR Residuals — Choropleth per Tessellation

Map the **mean absolute residual** onto each tessellation type, producing
clean filled maps so you can compare how tessellation geometry aggregates
model error.

```{r gwr-residual-maps, eval=exists("gwr_fit") && !is.null(gwr_fit), results='asis'}
pts_with_gwr <- points_sf
pts_with_gwr$gwr_fitted   <- as.numeric(fitted(gwr_fit))
pts_with_gwr$gwr_residual <- as.numeric(residuals(gwr_fit))
pts_with_gwr$abs_error    <- abs(pts_with_gwr$gwr_residual)

tess_list <- list(
  list(tess = tess_voronoi, label = "Voronoi"),
  list(tess = tess_hex,     label = "Hex Grid"),
  list(tess = tess_square,  label = "Square Grid")
)

for (info in tess_list) {
  cells <- info$tess$cells
  id_col <- if ("cell_id" %in% names(cells)) "cell_id" else "poly_id"
  if (!id_col %in% names(cells)) {
    cells$cell_id <- seq_len(nrow(cells))
    id_col <- "cell_id"
  }

  asgn <- assign_features_to_polygons(pts_with_gwr, cells, polygon_id_col = id_col)

  cell_err <- asgn |>
    st_drop_geometry() |>
    group_by(.data[[id_col]]) |>
    summarise(mean_abs_error = mean(abs_error, na.rm = TRUE), .groups = "drop")

  cells <- left_join(cells, cell_err, by = id_col)

  p <- plot_tessellation_map(
    tessellation_sf = cells,
    boundary        = nc_boundary,
    fill_col        = "mean_abs_error",
    palette         = "magma",
    tile_alpha      = 0.9,
    outline_col     = "white",
    outline_size    = 0.3,
    boundary_col    = "grey20",
    boundary_size   = 0.8,
    legend_title    = "Mean |Residual|",
    title           = sprintf("GWR Residuals — %s", info$label),
    subtitle        = sprintf("Abs. residual aggregated to %d cells", nrow(cells))
  )
  print(p)
  cat("\n\n")
}
```

---

## 7. Cross-Validation (5-Fold GWR)

```{r cv-gwr}
cv_results <- tryCatch({
  cv_gwr(
    data_sf        = points_sf,
    response_var   = response_var,
    predictor_vars = predictor_vars,
    k              = 5,
    adaptive       = TRUE
  )
}, error = function(e) {
  message("CV skipped: ", conditionMessage(e))
  NULL
})
```

```{r cv-results, eval=exists("cv_results") && !is.null(cv_results)}
cat(sprintf("CV RMSE: %.3f  |  CV R²: %.3f  |  CV MAE: %.3f\n",
            cv_results$summary$rmse,
            cv_results$summary$r_squared,
            cv_results$summary$mae))
```

---

## Summary

| Tessellation | Cells | Notes |
|:---|---:|:---|
| Voronoi | `r nrow(tess_voronoi$cells)` | Adapts to point density via k-means seeds |
| Hex grid | `r nrow(tess_hex$cells)` | Uniform hexagons, good for regular sampling |
| Square grid | `r nrow(tess_square$cells)` | Simplest regular grid |
| Delaunay | `r if(!is.null(tess_tri)) nrow(tess_tri$cells) else "—"` | One triangle per point triplet, finest resolution |

The choropleth maps show how each tessellation aggregates the response
variable spatially across North Carolina. The GWR residual maps let you
compare how each tessellation captures model error — hexes and squares give a
uniform view, while Voronoi cells highlight where the observation network is
dense or sparse.

```{r session-info, echo=FALSE}
sessionInfo()
```
