--- title: "wkpool: Topology-based Geometry Handling" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{wkpool: Topology-based Geometry Handling} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(wkpool) ``` ## Why topology? **wkpool** represents geometry as segments referencing a shared vertex pool. Topology emerges naturally: shared edges, neighbours, and ring structure become simple lookups. Simple features store geometry as complete coordinate sequences per feature. Two adjacent polygons duplicate their shared boundary — wasteful in entropy and requiring spatial predicates (GEOS) to rediscover adjacency. ## Basic workflow Create two adjacent squares sharing an edge: ```{r basic-setup} library(wk) library(wkpool) # Two adjacent unit squares poly1 <- wkt("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))") poly2 <- wkt("POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))") geoms <- c(poly1, poly2) ``` ### Establish topology ```{r establish} pool <- establish_topology(geoms) pool ``` At this stage, vertices are indexed but not yet merged — the shared edge exists as duplicate coordinate pairs. ### Inspect the raw state ```{r report-before} topology_report(pool) ``` ### Access the pool components ```{r accessors} pool_vertices(pool) pool_segments(pool) pool_feature(pool) ``` ### Merge coincident vertices ```{r merge} pool <- merge_coincident(pool) topology_report(pool) ``` After merging, shared edge vertices reference the same pool indices. ## Adjacency discovery ### Shared edges ```{r shared-edges} find_shared_edges(pool) ``` Returns segment indices that appear in multiple features. ### Internal boundaries For polygon data, shared edges with *opposite* winding indicate true internal boundaries (not self-shared rings): ```{r internal-boundaries} find_internal_boundaries(pool) ``` ### Neighbour graph ```{r neighbours} find_neighbours(pool) ``` Returns a data frame of feature pairs that share an edge. ## Ring and cycle analysis Topology enables ring discovery without parsing WKT structure. ### Find closed cycles ```{r cycles} cycles <- find_cycles(pool) cycles ``` ### Classify by winding Signed area distinguishes outer rings from holes. Following the SF convention, negative area indicates an outer ring, positive indicates a hole: ```{r winding} # Area of the first cycle cycle_signed_area(cycles[[1]], pool_vertices(pool)) # Classify all cycles classify_cycles(pool) ``` Use `reverse_cycle()` to flip winding if needed. ## Arc-node topology Arcs are maximal sequences of segments passing through degree-2 vertices. Nodes are vertices where degree ≠ 2 (branch points or endpoints). ```{r arc-node} vertex_degree(pool) find_nodes(pool) find_arcs(pool) arc_node_summary(pool) ``` ## Round-trip to WKT Convert topology back to standard geometry formats: ```{r round-trip} # Arcs as linestrings arcs_to_wkt(pool) # Cycles as polygons cycles_to_wkt(pool) # Raw segments segments_to_wkt(pool, type = "linestring")[1:3] ``` ## Triangulation integration The pool maps directly to constrained triangulation inputs. ### RTriangle (PSLG) ```{r pslg} pslg <- as_pslg(pool) str(pslg) ``` For polygons with holes, use `hole_points()` to generate interior points that tell the triangulator which regions to exclude: ```{r triangulate, eval = requireNamespace("RTriangle", quietly = TRUE)} # A polygon with a hole holed <- wk::as_wkb( "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 3 1, 3 3, 1 3, 1 1))" ) hp <- establish_topology(holed) hp <- merge_coincident(hp) pslg_h <- as_pslg(hp) holes <- hole_points(hp) tri <- RTriangle::triangulate( RTriangle::pslg(P = pslg_h$P, S = pslg_h$S, H = holes) ) ``` ```{r decido, include = FALSE, eval = FALSE} ### decido (earcut) dec <- as_decido(pool) str(dec) ``` ## Subsetting and combining Pools support `[` subsetting — the full vertex pool is retained: ```{r subset} pool[1:4] ``` Combine pools from different sources: ```{r combine} pool_a <- establish_topology(wk::wkt("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")) pool_b <- establish_topology(wk::wkt("POLYGON ((5 5, 6 5, 6 6, 5 6, 5 5))")) pool_combine(pool_a, pool_b) ``` ## Visualization ```{r plot, fig.width = 6, fig.height = 4} plot(pool) ``` ## Related Work ### Core topology/vertex-pool (hypertidy ecosystem) | Package | Where | Relationship to wkpool | |---------|-------|------------------------| | [silicate](https://CRAN.R-project.org/package=silicate) | CRAN | Mature predecessor — `SC` (edge-vertex), `ARC` (shared boundaries), `PATH` models. wkpool is the `wk`-native successor | | [unjoin](https://CRAN.R-project.org/package=unjoin) | CRAN | Vertex deduplication via data frame normalization — used internally by silicate | | [wk](https://CRAN.R-project.org/package=wk) | CRAN | Path/geometry indexing via `wk_coords()` | ### Mesh/triangulation | Package | Where | Notes | |---------|-------|-------| | [RTriangle](https://CRAN.R-project.org/package=RTriangle) | CRAN | Shewchuk's Triangle — constrained Delaunay, `as_pslg()` target | | [decido](https://CRAN.R-project.org/package=decido) | CRAN | Earcut — fast, returns vertex indices (implicit pool), `as_decido()` target | | [sfdct](https://CRAN.R-project.org/package=sfdct) | CRAN | RTriangle wrapper for sf objects | ### Adjacency (computed, not stored) | Package | Where | Approach | |---------|-------|----------| | [sfdep](https://CRAN.R-project.org/package=sfdep) | CRAN | `st_contiguity()` computes on-demand | | [sf](https://CRAN.R-project.org/package=sf) | CRAN | `st_touches()`, `st_relate()` via GEOS — recomputes each time | **Key distinction**: wkpool differs from sf/spdep by *storing* topology (shared edges are explicit) rather than *computing* it on demand via GEOS. It's a lightweight, `wk`-native evolution of silicate's approach.