---
title: "Practical Workflows"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Practical Workflows}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
source("_common.R")
library(hexify)
library(sf)
library(ggplot2)
```

Shared grids across datasets, polygon generation, resolution selection, and GIS export.

## One Grid, Many Datasets

Define a grid once, reuse it everywhere. Same grid object = guaranteed spatial alignment across datasets.

### The Problem

You often have:

- Several independent datasets (observations, sensors, surveys)

- All in longitude/latitude coordinates

- Collected at different times or from different sources

You want to:

- Put everything on one common global grid

- Be sure the grids actually match

- Combine results later without subtle errors

### The Solution: Shared Grid Objects

```{r one-grid-many-datasets}
# Step 1: Define the grid once
# This is your shared spatial reference system
grid <- hex_grid(area_km2 = 5000)
print(grid)

# Step 2: Create multiple datasets with different structures
set.seed(123)

# Dataset 1: Bird observations (note different column names)
bird_obs <- data.frame(
  species = sample(c("Passer domesticus", "Turdus merula", "Parus major"), 50, replace = TRUE),
  longitude = runif(50, -10, 30),
  latitude = runif(50, 35, 60)
)

# Dataset 2: Mammal records
mammal_obs <- data.frame(
  species = sample(c("Vulpes vulpes", "Meles meles", "Sciurus vulgaris"), 40, replace = TRUE),
  lon = runif(40, -10, 30),
  lat = runif(40, 35, 60)
)

# Dataset 3: Climate stations
climate_data <- data.frame(
  station_id = paste0("WS", 1:20),
  x = runif(20, -10, 30),
  y = runif(20, 35, 60),
  temp_c = rnorm(20, 12, 5)
)

# Step 3: Attach all datasets to the SAME grid
birds <- hexify(bird_obs, lon = "longitude", lat = "latitude", grid = grid)
mammals <- hexify(mammal_obs, lon = "lon", lat = "lat", grid = grid)
climate <- hexify(climate_data, lon = "x", lat = "y", grid = grid)

cat("Birds:  ", nrow(as.data.frame(birds)), "observations in", n_cells(birds), "cells\n")
cat("Mammals:", nrow(as.data.frame(mammals)), "observations in", n_cells(mammals), "cells\n")
cat("Climate:", nrow(as.data.frame(climate)), "stations in", n_cells(climate), "cells\n")
```

### Combining at the Cell Level

Once data are hexified, longitude/latitude no longer matter for analysis. The `cell_id` becomes the shared spatial key:

```{r cell-level-analysis}
# Extract data frames with cell IDs
birds_df <- as.data.frame(birds)
birds_df$cell_id <- birds@cell_id

mammals_df <- as.data.frame(mammals)
mammals_df$cell_id <- mammals@cell_id

climate_df <- as.data.frame(climate)
climate_df$cell_id <- climate@cell_id

# Aggregate each dataset by cell
bird_richness <- aggregate(
  species ~ cell_id,
  data = birds_df,
  FUN = function(x) length(unique(x))
)
names(bird_richness)[2] <- "bird_species"

mammal_richness <- aggregate(
  species ~ cell_id,
  data = mammals_df,
  FUN = function(x) length(unique(x))
)
names(mammal_richness)[2] <- "mammal_species"

mean_temp <- aggregate(
  temp_c ~ cell_id,
  data = climate_df,
  FUN = mean
)
names(mean_temp)[2] <- "mean_temp"

# Join datasets by cell_id - guaranteed to align because same grid
combined <- merge(bird_richness, mammal_richness, by = "cell_id", all = TRUE)
combined <- merge(combined, mean_temp, by = "cell_id", all = TRUE)

head(combined)
```

## Grid Generation

hexify provides functions to generate grid polygons over regions for visualization and analysis.

### Grid Over a Rectangular Region

```{r grid-rect, fig.width=7, fig.height=5}
# Create grid specification
grid <- hex_grid(area_km2 = 5000)

# Generate hexagons over Western Europe
europe_hexes <- grid_rect(c(-10, 35, 25, 60), grid)

# Get European countries for context
europe <- hexify_world[hexify_world$continent == "Europe", ]

ggplot() +
  geom_sf(data = europe, fill = "gray95", color = "gray60") +
  geom_sf(data = europe_hexes, fill = NA, color = "steelblue", linewidth = 0.4) +
  coord_sf(xlim = c(-10, 25), ylim = c(35, 60)) +
  labs(title = sprintf("Hexagonal Grid (~%.0f km² cells)", grid@area_km2)) +
  theme_minimal(base_size = FIG_BASE_SIZE)
```

### Grid Over a Polygon (Shapefile)

Clip a grid to any sf polygon boundary:

```{r grid-polygon, fig.width=7, fig.height=7}
# Get France boundary
france <- hexify_world[hexify_world$name == "France", ]

# Generate grid covering mainland France
grid <- hex_grid(area_km2 = 2000)
france_grid <- grid_rect(c(-5, 41, 10, 52), grid)

# Clip grid to France boundary
france_grid_clipped <- st_intersection(france_grid, st_geometry(france))

ggplot() +
  geom_sf(data = france, fill = "gray95", color = "gray40", linewidth = 0.5) +
  geom_sf(data = france_grid_clipped, fill = alpha("steelblue", 0.3),
          color = "steelblue", linewidth = 0.3) +
  coord_sf(xlim = c(-5, 10), ylim = c(41, 52)) +
  labs(title = "Hexagonal Grid Clipped to France",
       subtitle = sprintf("~%.0f km² cells", grid@area_km2)) +
  theme_minimal(base_size = FIG_BASE_SIZE)
```

### Global Grid

```{r grid-global, fig.width=7, fig.height=3.5, warning=FALSE}
# Coarse global grid (be careful with fine grids - many cells!)
grid <- hex_grid(area_km2 = 500000)
global_hexes <- grid_global(grid)

ggplot() +
  geom_sf(data = hexify_world, fill = "gray90", color = "gray70", linewidth = 0.2) +
  geom_sf(data = global_hexes, fill = NA, color = "darkgreen", linewidth = 0.3) +
  labs(title = sprintf("Global Hexagonal Grid (~%.0f km² cells)", grid@area_km2)) +
  theme_minimal(base_size = FIG_BASE_SIZE) +
  theme(axis.text = element_blank(), axis.ticks = element_blank())
```

## Multi-Resolution Analysis

Analyze data at multiple spatial scales using different target areas.

```{r multi-resolution}
# Sample data
set.seed(42)
observations <- data.frame(
  species = sample(c("Species A", "Species B", "Species C"), 100, replace = TRUE),
  lon = runif(100, -10, 30),
  lat = runif(100, 35, 60)
)

# Fine resolution (~1000 km² cells)
grid_fine <- hex_grid(area_km2 = 1000)
obs_fine <- hexify(observations, lon = "lon", lat = "lat", grid = grid_fine)

# Coarse resolution (~10000 km² cells)
grid_coarse <- hex_grid(area_km2 = 10000)
obs_coarse <- hexify(observations, lon = "lon", lat = "lat", grid = grid_coarse)

cat(sprintf("Fine resolution: %d unique cells (area: %.1f km²)\n",
            n_cells(obs_fine), grid_fine@area_km2))
cat(sprintf("Coarse resolution: %d unique cells (area: %.1f km²)\n",
            n_cells(obs_coarse), grid_coarse@area_km2))
```

### Scale-Dependent Patterns

```{r multi-scale-aggregate}
# Extract data with cell IDs
fine_df <- as.data.frame(obs_fine)
fine_df$cell_id <- obs_fine@cell_id

coarse_df <- as.data.frame(obs_coarse)
coarse_df$cell_id <- obs_coarse@cell_id

# Species richness at each scale
richness_fine <- aggregate(species ~ cell_id, data = fine_df,
                           FUN = function(x) length(unique(x)))
richness_coarse <- aggregate(species ~ cell_id, data = coarse_df,
                             FUN = function(x) length(unique(x)))

cat(sprintf("Fine scale: mean %.2f species per cell\n", mean(richness_fine$species)))
cat(sprintf("Coarse scale: mean %.2f species per cell\n", mean(richness_coarse$species)))
```

## Spatial Joins

Join datasets based on shared grid cells rather than proximity.

```{r spatial-joins}
# Dataset 1: Weather stations
stations <- data.frame(
  station_id = paste0("ST", 1:50),
  lon = runif(50, -10, 30),
  lat = runif(50, 35, 60),
  temperature = rnorm(50, 15, 5)
)

# Dataset 2: Cities
cities <- data.frame(
  city = c("Vienna", "Paris", "London", "Berlin", "Rome",
           "Madrid", "Prague", "Warsaw", "Budapest", "Amsterdam"),
  lon = c(16.37, 2.35, -0.12, 13.40, 12.50,
          -3.70, 14.42, 21.01, 19.04, 4.90),
  lat = c(48.21, 48.86, 51.51, 52.52, 41.90,
          40.42, 50.08, 52.23, 47.50, 52.37)
)

# Use a coarse grid for joining disparate points
grid <- hex_grid(area_km2 = 50000)

# Hexify both datasets with the same grid
stations_hex <- hexify(stations, lon = "lon", lat = "lat", grid = grid)
cities_hex <- hexify(cities, lon = "lon", lat = "lat", grid = grid)

# Extract with cell IDs
stations_df <- as.data.frame(stations_hex)
stations_df$cell_id <- stations_hex@cell_id

cities_df <- as.data.frame(cities_hex)
cities_df$cell_id <- cities_hex@cell_id

# Join by cell_id
city_weather <- merge(
  cities_df[, c("city", "cell_id")],
  aggregate(temperature ~ cell_id, data = stations_df, FUN = mean),
  by = "cell_id",
  all.x = TRUE
)

city_weather
```

## Cell-Level Aggregation and Neighbors

`hex_summarize()` aggregates data per cell without manually splitting and
merging, and `get_neighbors()` finds the ring of cells surrounding a given
cell — useful for smoothing, spatial lag features, or checking what
surrounds a hotspot.

```{r cell-summarize}
# Aggregate the station data straight from the HexData object
hex_summarize(stations_hex, mean_temp = mean(temperature), n_stations = length(temperature))
```

```{r cell-neighbors}
# The 6 cells surrounding a given cell (k = 1), or a wider ring with k > 1
some_cell <- stations_hex@cell_id[1]
get_neighbors(some_cell, grid)
get_neighbors(some_cell, grid, k = 2)
```

## Choosing Resolution

### By Target Area

Use `hex_grid()` with `area_km2` to get the closest available resolution:

```{r choosing-resolution}
# Target: 100 km² cells
grid_100 <- hex_grid(area_km2 = 100, aperture = 3)
cat(sprintf("Target ~100 km²: resolution %d (actual: %.1f km²)\n",
            grid_100@resolution, grid_100@area_km2))

# Target: 1000 km² cells
grid_1000 <- hex_grid(area_km2 = 1000, aperture = 3)
cat(sprintf("Target ~1000 km²: resolution %d (actual: %.1f km²)\n",
            grid_1000@resolution, grid_1000@area_km2))

# Target: 10000 km² cells
grid_10000 <- hex_grid(area_km2 = 10000, aperture = 3)
cat(sprintf("Target ~10000 km²: resolution %d (actual: %.1f km²)\n",
            grid_10000@resolution, grid_10000@area_km2))
```

### Resolution Table (Aperture 3)

```{r resolution-table, echo=FALSE}
comparison <- hexify_compare_resolutions(aperture = 3, res_range = 0:15)
comparison$n_cells_fmt <- ifelse(
  comparison$n_cells > 1e6,
  sprintf("%.1fM", comparison$n_cells / 1e6),
  ifelse(comparison$n_cells > 1e3,
         sprintf("%.1fK", comparison$n_cells / 1e3),
         as.character(comparison$n_cells))
)
comparison$area_fmt <- ifelse(
  comparison$cell_area_km2 < 0.1,
  formatC(comparison$cell_area_km2, format = "e", digits = 1),
  formatC(comparison$cell_area_km2, format = "f", digits = 1, big.mark = ",")
)
comparison$spacing_fmt <- ifelse(
  comparison$cell_spacing_km < 0.1,
  formatC(comparison$cell_spacing_km, format = "e", digits = 1),
  formatC(comparison$cell_spacing_km, format = "f", digits = 1)
)
knitr::kable(
  comparison[, c("resolution", "n_cells_fmt", "area_fmt", "spacing_fmt")],
  col.names = c("Resolution", "# Cells", "Cell Area (km\u00b2)", "Spacing (km)"),
  align = c("r", "r", "r", "r")
)
```

### Comparing Apertures

Different apertures offer different resolution steps:

```{r compare-apertures}
target_area <- 1000  # km²

cat(sprintf("Target: ~%d km² cells\n\n", target_area))
for (ap in c(3, 4, 7)) {
  grid <- hex_grid(area_km2 = target_area, aperture = ap)
  n_cells <- 10 * (ap^grid@resolution) + 2
  cat(sprintf("Aperture %d: resolution %d -> %.1f km² (%s cells globally)\n",
              ap, grid@resolution, grid@area_km2,
              format(n_cells, big.mark = ",")))
}
```

| Aperture | Best For | Trade-offs |
|----------|----------|------------|
| 3 | Fine resolution control, dggridR compatibility | Slowest cell growth |
| 4 | Power-of-2 scaling, GIS workflows | Moderate resolution steps |
| 7 | Rapid cell count growth, coarse analysis | Largest resolution jumps |
| 4/3 | Balance of 4's fast start + 3's fine control | More complex indexing |

## Grids on Other Bodies

A grid partitions a sphere, and `radius_km` sets which sphere. Pass a radius in
kilometres or the name of a body:

```{r other-bodies}
mars <- hex_grid(area_km2 = 1000, radius_km = "mars")
mars

# The same grid, by radius
identical(mars@area_km2, hex_grid(resolution = mars@resolution, radius_km = 3389.5)@area_km2)
```

Cell geometry is angular: which cell a coordinate falls in, where centres and
corners sit, the parent-child hierarchy and the neighbours are the same on
every body. The radius sets the kilometre figures — cell area, diagonal,
spacing, and the resolution that a target `area_km2` picks.

```{r other-bodies-geometry}
earth <- hex_grid(resolution = 5)
moon  <- hex_grid(resolution = 5, radius_km = "moon")

lon <- c(0, 16.37, -70.5)
lat <- c(0, 48.21, -33.4)

identical(lonlat_to_cell(lon, lat, moon), lonlat_to_cell(lon, lat, earth))
c(earth = earth@area_km2, moon = moon@area_km2)
```

Areas come from the sphere of that radius, `4 * pi * r^2`; Earth keeps the
WGS84 ellipsoid area. Named bodies carry the IAU mean radii (Archinal et al.
2018) tabulated by JPL Solar System Dynamics: `mercury`, `venus`, `earth`,
`moon`, `mars`, `ceres`, `jupiter`, `io`, `europa`, `ganymede`, `callisto`,
`saturn`, `enceladus`, `titan`, `uranus`, `neptune`, `pluto`.

`radius_km` reaches every function that reports kilometres, because the grid
object carries it: `dgearthstat()`, `cell_area()`, `hexify_compare_resolutions()`
and the sf exports all read the grid's radius.

### Coordinates on another body

EPSG codes name Earth reference systems, so a grid built on another radius
carries a longlat CRS on the sphere of that radius:

```{r other-bodies-crs}
mars@crs
```

Every sf object built from that grid carries it, so cells, centres and hexified
data come back in the body's own coordinates. A planet of your own works the
same way through a radius in kilometres:

```{r other-bodies-crs-custom}
world <- hex_grid(area_km2 = 5000, radius_km = 4200)
world@crs
```

`crs` also takes a value of your own, as an EPSG code or a PROJ or WKT string,
which is how a projected system for the body reaches the grid:

```{r other-bodies-crs-explicit}
hex_grid(area_km2 = 5000, radius_km = 4200,
         crs = "+proj=laea +lat_0=0 +lon_0=0 +R=4200000 +no_defs")@crs
```

A basemap for such a body is any file sf or terra reads: `sf::st_read()` takes a
shapefile, GeoJSON or GeoPackage of coastlines, `terra::rast()` takes a GeoTIFF.
Both go to `hexify_heatmap(basemap = )`.

Both backends take a radius. H3 reports a cell's area as its solid angle times
Earth's radius squared, so another radius scales those areas by the square of
the radius ratio:

```{r other-bodies-h3}
h3_mars <- hex_grid(resolution = 5, type = "h3", radius_km = "mars")
h3_mars@area_km2 / hex_grid(resolution = 5, type = "h3")@area_km2

# H3 measures against the WGS84 authalic radius, so that is what divides out
(3389.5 / 6371.007180918475)^2
```

One caveat rides along with H3. A cell ID names a position in H3's topology,
which Uber's H3 reads on Earth. A grid on another body reuses that topology and
its own radius for areas, so its IDs are that topology on that body, not
interchangeable with Earth H3 data. `h3_crosswalk()` needs both grids on the
same body for the same reason.

## Working with sf

### Convert HexData to sf

```{r sf-workflow}
# Hexify some data
grid <- hex_grid(area_km2 = 20000)
result <- hexify(cities, lon = "lon", lat = "lat", grid = grid)

# Convert to sf points (uses cell centers)
sf_points <- as_sf(result, geometry = "point")
class(sf_points)

# Convert to sf polygons (for choropleth maps)
sf_polys <- as_sf(result, geometry = "polygon")
class(sf_polys)

# Or generate polygons directly from cell IDs
unique_cells <- cells(result)
cell_polys <- cell_to_sf(unique_cells, grid)
```

### Visualize with ggplot2

```{r sf-plot, fig.width=7, fig.height=5}
europe <- hexify_world[hexify_world$continent == "Europe", ]

ggplot() +
  geom_sf(data = europe, fill = "ivory", color = "gray70") +
  geom_sf(data = cell_polys, fill = "steelblue", alpha = 0.5, color = "darkblue") +
  coord_sf(xlim = c(-10, 25), ylim = c(35, 58)) +
  labs(title = "European Cities - Hexagonal Grid") +
  theme_minimal(base_size = FIG_BASE_SIZE)
```

## Export to GIS Formats

Use sf's `st_write()` to export grids for use in external GIS software:

```{r export-formats, eval=FALSE}
# Generate a grid
grid <- hex_grid(area_km2 = 10000)
europe <- grid_rect(c(-10, 35, 25, 60), grid)

# Export to various formats
st_write(europe, "europe_grid.gpkg", layer = "hexgrid")     # GeoPackage
st_write(europe, "europe_grid.shp")                         # Shapefile
st_write(europe, "europe_grid.geojson")                     # GeoJSON
st_write(europe, "europe_grid.kml", layer = "hexgrid")      # KML (Google Earth)
```

## Edge Cases

### Handling NA Coordinates

```{r edge-cases}
data_with_na <- data.frame(
  lon = c(16.37, NA, 2.35, 13.40),
  lat = c(48.21, 48.86, NA, 52.52)
)

grid <- hex_grid(area_km2 = 1000)
result <- hexify(data_with_na, lon = "lon", lat = "lat", grid = grid)

# Check which rows have valid cell assignments
cat("Cell IDs:", result@cell_id, "\n")
cat("NA indicates invalid coordinates\n")
```

### Polar Regions

Coordinates with latitude > 89° may have projection artifacts. The grid remains valid, but polygon visualization can be distorted near poles.

### Date Line

Polygons crossing the date line (lon = ±180°) are handled automatically.
`cell_to_sf()` applies `sf::st_wrap_dateline()` internally, so flat map
projections render correctly without manual intervention.

## Function Summary

| Task | Function |
|------|----------|
| Create grid specification | `hex_grid(area_km2 = ...)` |
| Assign points to cells | `hexify(df, lon, lat, grid)` |
| Get grid from HexData | `grid_info(result)` |
| Get unique cell IDs | `cells(result)` |
| Count cells | `n_cells(result)` |
| Extract data frame | `as.data.frame(result)` |
| Convert to sf | `as_sf(result, geometry = "polygon")` |
| Generate polygons | `cell_to_sf(cell_ids, grid)` |
| Grid over region | `grid_rect(bbox, grid)` |
| Global grid | `grid_global(grid)` |
| Coordinate conversion | `lonlat_to_cell()`, `cell_to_lonlat()` |
| Compare resolutions | `hexify_compare_resolutions()` |
| Aggregate data per cell | `hex_summarize(result, ...)` |
| Find neighboring cells | `get_neighbors(cell_id, grid, k = ...)` |
| Merge/split multi-resolution cells | `hex_compact()`, `hex_uncompact()` |
| Distance between cells | `hex_distance(cell_id1, cell_id2, grid)` |
| Extract raster values at cell centers | `hex_extract(raster, grid)` |
| Zonal statistics over cell polygons | `hex_zonal(raster, grid)` |
| Map ISEA cells to H3 (or vice versa) | `h3_crosswalk()` |
| Per-cell area | `cell_area(cell_ids, grid)` |

## See Also

- `vignette("quickstart")` - Getting started with hexify

- `vignette("visualization")` - Plotting with `plot()`, `hexify_heatmap()`

- `vignette("theory")` - Mathematical foundations (ISEA projection, apertures)
