Merge coincident vertices in a pool
Arguments
- x
A wkpool, or a
wkpool_tri(seepool_triangulate())- tolerance
Numeric tolerance for coordinate matching. Default 0 means exact match only.
- period_x
NULL(default) for no wrapping, or a single positive number: x is treated as periodic with this period when deciding which vertices coincide (for example 360 for longitude in degrees). See 'Periodic x' below.
Details
With tolerance = 0, only exactly identical coordinates are merged - identity is the double-precision bit pattern (grouped at C level via vctrs), not a formatted representation, so values that differ only beyond printable precision remain distinct. The first occurrence becomes the canonical vertex; coordinates are never moved.
With a nonzero tolerance, coordinates are snapped to a grid of cell
size tolerance for matching (the canonical vertex keeps its
original coordinates). Note this is a snap grid, not a distance
guarantee: two vertices within tolerance of each other that
straddle a grid-cell boundary do not merge.
When the pool carries z, vertices are keyed on all coordinate
columns: equal x/y with different z remain distinct vertices.
This is where you discover shared boundaries between polygons, network connectivity, mesh topology, etc.
Periodic x
With period_x set, vertices are keyed on x %% period_x (then on the
tolerance snap grid, if any, wrapped again so that the grid cell at the
period boundary is the same cell as the one at zero). This fuses the
two copies of a seam, for example x = -180 and x = 180 for longitude,
and with period_x = 360 data in a 0 to 360 convention fuses at 0 and
360 in the same way. Once fused, a seam is an ordinary internal
boundary: find_shared_edges(), find_internal_boundaries() and the
quotient argument of find_arcs() treat it like any other shared
boundary.
The period is never inferred: wkpool carries the crs without interpreting it, so it cannot know that x is longitude. The decision to fuse is made here, explicitly.
Coordinates are still never moved: the canonical vertex keeps the x of
its first occurrence (say -180), and segments from the other side of the
seam now reference it. The topology is correct on the cylinder, but
geometry emitted from the pool (for example by cycles_to_wkb()) has
coordinate jumps of about one period across the seam. Consumers that
understand wrapping (s2, great-circle densification) are fine; a planar
consumer needs the seam split again downstream.
Segments are identified by their two vertices, so on the cylinder a
segment cannot say which way round it goes. Two segments whose ends
fuse to the same pair but which run opposite ways around become the
same edge, for example the edge from -180 to 0 in one feature and from
0 to 180 in another. This needs a segment that spans at least half the
period, so densify long segments before merging with period_x.
Only x is periodic. Poles (where all x coincide on the sphere), periodic y, and any detection from the crs are out of scope.
Feature provenance (.feature) is preserved - segments keep their original feature assignment even after vertex merging.
Examples
x <- wk::as_wkb(c(
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
"POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
))
pool <- establish_topology(x)
topology_report(pool)
#> $n_vertices_raw
#> [1] 10
#>
#> $n_vertices_unique
#> [1] 6
#>
#> $n_duplicate_vertices
#> [1] 4
#>
#> $n_near_miss_vertices
#> [1] 0
#>
#> $n_segments
#> [1] 8
#>
#> $n_shared_edges
#> [1] 1
#>
#> $n_features
#> [1] 2
#>
merged <- merge_coincident(pool)
topology_report(merged)
#> $n_vertices_raw
#> [1] 6
#>
#> $n_vertices_unique
#> [1] 6
#>
#> $n_duplicate_vertices
#> [1] 0
#>
#> $n_near_miss_vertices
#> [1] 0
#>
#> $n_segments
#> [1] 8
#>
#> $n_shared_edges
#> [1] 1
#>
#> $n_features
#> [1] 2
#>
# two hemispheres, each with its own copy of the antimeridian
g <- wk::as_wkb(c(
"POLYGON ((-180 -60, 0 -60, 0 60, -180 60, -180 -60))",
"POLYGON ((0 -60, 180 -60, 180 60, 0 60, 0 -60))"
))
pool <- establish_topology(g)
nrow(pool_vertices(merge_coincident(pool)))
#> [1] 6
fused <- merge_coincident(pool, period_x = 360)
nrow(pool_vertices(fused))
#> [1] 4
find_shared_edges(fused)
#> edge_key .vx0 .vx1 .feature segment_idx features
#> 1 1-2 1 2 1 1 1, 2
#> 2 2-3 2 3 1 2 1, 2
#> 3 3-4 3 4 1 3 1, 2
#> 4 1-4 1 4 1 4 1, 2
#> 5 1-2 1 2 2 5 1, 2
#> 6 1-4 1 4 2 6 1, 2
#> 7 3-4 3 4 2 7 1, 2
#> 8 2-3 2 3 2 8 1, 2