---
title: "Introduction to sfext"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Introduction to sfext}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup, message = FALSE}
library(sfext)
library(dplyr)
library(ggplot2)

theme_set(theme_void())
```

The {sfext} package has several categories of functions:

- Functions for reading and writing spatial data
- Converting the class or geometry of objects 
- Modifying sf, sfc, and bbox objects
- Getting information about sf, sfc, and bbox objects
- Working with units and scales

## Reading and writing sf objects

### Reading sf objects

`read_sf_ext()` calls one of four other functions depending on the input parameters:

- `read_sf_path()`
- `read_sf_url()` 
- `read_sf_pkg()` 
- `read_sf_query()` 

`read_sf_path()` is similar to `sf::read_sf` but has a few additional features. It checks the existence of a file before reading, supports the creation of wkt_filter parameters based on a bounding box, and supports conversion of tabular data to simple feature objects.

```{r}
nc <- read_sf_ext(path = system.file("shape/nc.shp", package = "sf"))
# This is equivalent to read_sf_path(system.file("shape/nc.shp", package = "sf"))
# Or sf::read_sf(dsn = system.file("shape/nc.shp", package = "sf"))

glimpse(nc)
```

```{r}
bbox <- as_bbox(nc[10, ])

nc_in_bbox <- read_sf_ext(path = system.file("shape/nc.shp", package = "sf"), bbox = bbox)

nc_basemap <-
  ggplot() +
  geom_sf(data = nc)

nc_basemap +
  geom_sf(data = nc_in_bbox, fill = "red")
```

The second, `read_sf_url()`, that supports reading url that are already supported by `sf::read_sf` but also supports ArcGIS Feature Layers (using the [{esri2sf}](https://github.com/elipousson/esri2sf) package) and URLs for tabular data (including both CSV files and Google Sheets). Several functions also support queries based on a name and name_col value (generating a simple SQL query) based on the provided values.

```{r}
sample_esri_url <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Boundaries_2022/FeatureServer/1"
states <- read_sf_esri(url = sample_esri_url)
# This is equivalent to read_sf_url(sample_esri_url)
# Or esri2sf::esri2sf(url = sample_esri_url)

# read_sf_esri and read_sf_query both support the name and name_col parameters
# These parameters also work with read_sf_pkg for cached and extdata files
nc_esri <- read_sf_ext(url = sample_esri_url, name_col = "STATE_NAME", name = "North Carolina")

ggplot() +
  geom_sf(data = states) +
  geom_sf(data = nc_esri, fill = "red")
```

The read functions also support URLs for GitHub Gists (assuming the first file in the Gist is a spatial data file) or Google MyMaps.

```{r}
gmap_data <- read_sf_ext(url = "https://www.google.com/maps/d/u/0/viewer?mid=1CEssu_neU7lx_vAZs5qpufOBoUQ&ll=-3.81666561775622e-14%2C0&z=1")

ggplot() +
  geom_sf(data = gmap_data[2, ])
```

The third, `read_sf_pkg()`, can load spatial data from any installed package including exported data, files in the extdata folder, or data in a package-specific cache folder. This is particularly useful when working with spatial data packages such as [{mapbaltimore}](https://elipousson.github.io/mapbaltimore/) or [{mapmaryland}](https://elipousson.github.io/mapmaryland/).

The fourth, `read_sf_query()`, is most similar to `sf::read_sf` but provides an optional spatial filter based on the bbox parameter and supports the creation of queries using a basic name and name_col parameter.

### Writing sf objects

`write_sf_ext()` wraps `sf::write_sf` but uses `filenamr::make_filename()` to support the creation of consistent file names using labels or date prefixes to organize exports. 

```{r}
filenamr::make_filename(
  name = "Ashe County",
  label = "NC",
  prefix = "date",
  fileext = "gpkg"
)
```

`write_sf_ext()` also supports the automatic creation of destination folders and the use of a package-specific cache folder created by `rappdirs::user_cache_dir()`. These functions are now in the `filenamr` package.

## Converting sf objects

### Checking and converting sf objects

There are several helper functions that are used extensively by the package itself. While these conversions are easy to do with existing {sf} functions, these alternatives follow a tidyverse style syntax and support a wider range of input values.

```{r}
is_sf(nc)

is_sfc(nc$geometry)

nc_bbox <- as_bbox(nc)

is_bbox(nc_bbox)
```

The ext parameter can be used to make `is_sf()` more general allowing sf, sfc, or bbox objects instead of just sf objects.

```{r}
is_sf(nc$geometry)

is_sf(nc$geometry, ext = TRUE)
```

There are similar functions for checking and converting the geometry type for an object including `is_point()`, `is_polygon()`, `is_line()`, and others.

```{r}
is_point(nc)

is_point(suppressWarnings(sf::st_centroid(nc)))

# as_point returns an sfg object
is_sfg(as_point(nc))

# as_points returns an sfc object (and accepts numeric inputs as well as sfg)
is_sfc(as_points(c(-79.40065, 35.55937), crs = 4326))
```

### Converting to and from data frames

By default the conversion from sf to data frame object, uses the centroid of any polygon. It can also use a surface point from `sf::st_point_on_surface`

```{r}
df_centroid <- sf_to_df(nc_in_bbox)
df_surface_point <- sf_to_df(nc_in_bbox, geometry = "surface point")
```

These can be converted back to an sf object but the point geometry is used instead of the original polygon:

```{r}
df_centroid_sf <- df_to_sf(df_centroid, crs = 3857)
df_surface_point_sf <- df_to_sf(df_surface_point, crs = 3857)

df_example_map <-
  ggplot() +
  geom_sf(data = nc_in_bbox) +
  geom_sf(data = df_centroid_sf, color = "red", size = 3) +
  geom_sf(data = df_surface_point_sf, color = "blue", size = 2)

df_example_map
```

Alternatively, `sf_to_df()` can also use well known text as an output format:

```{r}
df_wkt <- sf_to_df(nc_in_bbox, geometry = "wkt")

glimpse(df_wkt)

df_example_map +
  geom_sf(data = df_to_sf(df_wkt), color = "orange", fill = NA)
```

The [tidygeocoder](https://jessecambon.github.io/tidygeocoder/) package is also used to support conversion of address vectors or data frames.

```{r}
address_to_sf(x = c("350 Fifth Avenue, New York, NY 10118"))
```

## Modifying sf objects

The next group of functions often provide similar functionality to standard {sf} functions but, again, allow a wider range of inputs and outputs or offer extra features. For example, `st_bbox_ext()` also wraps `sf::st_buffer` and uses a helper function based on `units::set_units` to a buffer distance of any valid distance unit.  

```{r}
nc_bbox <- st_bbox_ext(nc, class = "sf")
# Similar to sf::st_sf(sf::st_as_sfc(sf::st_bbox(nc)))

nc_bbox_buffer <- st_bbox_ext(nc, dist = 50, unit = "mi", class = "sf")
# Similar to sf::st_buffer(nc, dist = units::as_units(50, "mi"))

nc_basemap +
  geom_sf(data = nc_bbox, fill = NA, color = "blue") +
  geom_sf(data = nc_bbox_buffer, fill = NA, color = "red")
```

Other notable functions in this category include:

- `st_transform_ext()`: takes sf, sfc, or bbox objects as the crs parameter for `sf::st_transform()` and supports lists of sf objects (look for the `allow_list = TRUE` parameter to see which functions support lists)
- `st_union_ext()`: unions geometry while also collapsing a name column into a single character string using the `cli::pluralize` function
- `st_erase()`: checks the validity of inputs with `sf::st_is_valid` before and after using `sf::st_difference` or (if flip = TRUE) `sf::st_difference`

## Getting information about sf, sfc, and bbox objects

There are also several functions that return information about the geometry of input objects. Typically, these functions bind the information as a new column to an existing sf input (and convert sfc input objects to sf results).

The `get_length()` function wraps `sf::st_length` and (for POLYGON geometries only) `lwgeom::st_perimeter`:

```{r}
example_line <- as_line(as_point(nc[1, ]), as_point(nc[2, ]), crs = 4326)

glimpse(get_length(example_line))
```

The `get_area()` function wraps `sf::st_area()`:

```{r}
glimpse(get_area(nc[1:3, ], unit = "mi^2"))
```

`get_dist()` supports a more varied range of options including using the "to" parameter to define a corner or center of the input sf object bounding box.

```{r}
nc <- sf::st_transform(nc, 3857)

# use drop = TRUE, to drop the units class and return a numeric column
dist_example_min <- get_dist(nc, to = c("xmin", "ymin"), unit = "mi", drop = TRUE)

glimpse(select(dist_example_min, NAME, dist))

nc_basemap +
  geom_sf(data = dist_example_min, aes(fill = dist), alpha = 0.5)

dist_example_mid <- get_dist(nc, to = c("xmid", "ymid"), unit = "mi", drop = TRUE)

nc_basemap +
  geom_sf(data = dist_example_mid, aes(fill = dist), alpha = 0.5)
```

