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

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 5
)
has_decido <- requireNamespace("decido", quietly = TRUE)
has_rtriangle <- requireNamespace("RTriangle", quietly = TRUE)
has_silicate <- requireNamespace("silicate", quietly = TRUE)
```

A polygon in wkpool is a set of rings, and a ring is a cycle of segments:
one-dimensional topology. Triangles are the two-dimensional version, and
they can live on the same vertex pool. A `wkpool_tri` is a vector of
triangles whose corners are vertex ids into a pool, just as a wkpool is a
vector of segments.

`wkpool_tri` and `pool_triangulate()` are **experimental** (new in 0.4.0)
and may change. The older route, `as_pslg()` and `pool_holes()` feeding
RTriangle, is unchanged and is covered below alongside it.

```{r setup}
library(wkpool)
```

## Ear cutting

`pool_triangulate()` ear cuts each polygon with decido. Ear cutting adds
no vertices, so the triangles reference the pool's vertices as they are.

```{r earcut, eval = has_decido}
x <- wk::as_wkb(c(
  "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 3, 2 2, 3 3, 3 1, 1 1))",
  "POLYGON ((4 0, 6 0, 6 4, 4 4, 4 0))"
))
pool <- establish_topology(x)
tri <- pool_triangulate(pool)
tri
pool_triangles(tri)
```

```{r earcut-plot, eval = has_decido, fig.height = 4}
plot(tri, asp = 1)
```

Each triangle keeps the feature and part it came from, and triangles are
counter-clockwise. `wk_handle()` gives one POLYGON per triangle, so any wk
consumer can take them:

```{r earcut-wk, eval = has_decido}
wk::as_wkt(tri)[1:3]
```

Triangulating a raw pool and merging later gives the same result as
merging first. After merging, the two features share the corners on their
common edge. `tri_segments()` gives the triangle edges as an ordinary
wkpool, so the segment tools apply; here the one edge the features share,
once from each side:

```{r earcut-merge, eval = has_decido}
tri <- merge_coincident(tri)
edges <- tri_segments(tri)
find_shared_edges(edges)
```

## A real coverage

`inlandwaters` from silicate is six regions of Australia (and New
Caledonia) with many holes and long shared boundaries.

```{r inlandwaters, eval = has_decido && has_silicate, message = FALSE}
op <- par(mar = rep(0, 4L))
data("inlandwaters", package = "silicate")
# the geometry column as WKB
iw_geom <- wk::wk_handle(inlandwaters$geom, wk::wkb_writer())
iw <- merge_coincident(establish_topology(iw_geom))
iw_tri <- pool_triangulate(iw)
length(iw_tri)
nrow(pool_vertices(iw_tri)) == nrow(pool_vertices(iw))
plot(iw_tri, border = NA, asp = 1, axes = FALSE, xlab = "", ylab = "")
par(op)
```

`plot()` draws every triangle in one `polygon()` call indexed into the
pool, so tens of thousands of triangles draw in well under a second.

The view is much bigger than the land. Three small, distant islands set
the extent: Macquarie Island far to the south, which is one part of the
Tasmania feature; Lord Howe Island to the east, one part of the New South
Wales feature; and the feature labelled New Caledonia to the north east.
Each triangle records its feature and part, so the islands can be found
from the triangles themselves and dropped with a subset. The plot extent
follows the triangles that are drawn:

```{r inlandwaters-trim, eval = has_decido && has_silicate}
op <- par(mar = rep(0, 4L))
tt <- pool_triangles(iw_tri)
ev <- pool_vertices(iw_tri)
tx <- ev$x[match(tt$.vx0, ev$.vx)]
ty <- ev$y[match(tt$.vx0, ev$.vx)]
prov <- inlandwaters$Province
nsw <- tt$.feature == which(prov == "New South Wales")
islands <- c(
  "Macquarie Island" = tt$.part[which.min(ty)],
  "Lord Howe Island" = tt$.part[nsw][which.max(tx[nsw])],
  "New Caledonia" = unique(tt$.part[tt$.feature == which(prov == "New Caledonia")])
)
islands
plot(iw_tri[!tt$.part %in% islands], border = NA, asp = 1, axes = FALSE,
     xlab = "", ylab = "")
par(op)
```

## Constrained Delaunay: the PSLG route

Ear cutting gives valid triangles but they are often long and thin, and
it cannot add vertices. For good-quality triangles (minimum angle, maximum
area) you need a constrained Delaunay triangulation, and its input is a
planar straight line graph (PSLG): vertices, and segments that must appear
as triangle edges. A merged pool is already a PSLG; `as_pslg()` just
gives it as matrices, `P` for the vertices and `S` for the segments, in
the form RTriangle uses.

```{r pslg}
p <- as_pslg(establish_topology(wk::as_wkb(
  "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 3, 2 2, 3 3, 3 1, 1 1))"
)))
str(p)
```

A PSLG does not say which side of a segment is inside. RTriangle removes
the triangles outside the outer boundary, and needs a point in each hole
to remove those too. `pool_holes()` gives one point inside each hole ring
(a hole that another polygon fills exactly, like an enclave, is not a
hole of the coverage and is left out):

```{r rtriangle, eval = has_rtriangle, fig.height = 4}
h <- pool_holes(merge_coincident(establish_topology(wk::as_wkb(
  "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 3, 2 2, 3 3, 3 1, 1 1))"
))))
h
rt <- RTriangle::triangulate(RTriangle::pslg(P = p$P, S = p$S, H = h),
                             q = 30, a = 0.1)
plot(rt$P, asp = 1, pch = ".", xlab = "", ylab = "")
polygon(rt$P[t(cbind(rt$T, NA)), ], col = "grey90")
points(h, col = "firebrick", pch = 19)
```

RTriangle added vertices to meet the quality and area targets; those are
the Steiner points, appended after the input vertices.

## Back to a triangle pool

The result is a vertex table and an index table, which is all a
`wkpool_tri` is. The input vertices come first in the same order as the
pool, so their ids are unchanged and Steiner points get new ids after
them:

```{r back, eval = has_rtriangle}
v <- data.frame(.vx = seq_len(nrow(rt$P)), x = rt$P[, 1], y = rt$P[, 2])
rt_tri <- new_wkpool_tri(v, rt$T[, 1], rt$T[, 2], rt$T[, 3])
length(rt_tri)
rt_tri[1:3]
```

On real data the difference in triangle quality is plain. The first
feature of `inlandwaters` is the Australian Capital Territory: ear cut on
the left, long thin triangles fanning from a few vertices; through
RTriangle with a minimum angle of 25 degrees on the right, back on a
triangle pool:

```{r act-rt, eval = has_rtriangle && has_decido && has_silicate}
act <- iw_tri[pool_feature(iw_tri) == 1]
act_p <- as_pslg(establish_topology(iw_geom[1]))
act_rt <- RTriangle::triangulate(RTriangle::pslg(P = act_p$P, S = act_p$S),
                                 q = 25)
act_v <- data.frame(.vx = seq_len(nrow(act_rt$P)),
                    x = act_rt$P[, 1], y = act_rt$P[, 2])
act_rt_tri <- new_wkpool_tri(act_v, act_rt$T[, 1], act_rt$T[, 2], act_rt$T[, 3])
c(earcut = length(act), RTriangle = length(act_rt_tri))
op <- par(mfrow = c(1, 2), mar = rep(0.5, 4))
plot(act, col = "grey90", border = "grey40", asp = 1, axes = FALSE,
     xlab = "", ylab = "")
plot(act_rt_tri, col = "grey90", border = "grey40", asp = 1, axes = FALSE,
     xlab = "", ylab = "")
par(op)
```

The PSLG route triangulates the whole pool at once and does not record
which feature a triangle belongs to; `pool_triangulate()` does, because it
works one polygon at a time.

On `inlandwaters` RTriangle covers the same area as ear cutting, up to
one thing a PSLG cannot express with hole points alone: a region
enclosed by neighbouring polygons that none of them covers is not a hole
of any ring, so `pool_holes()` gives no point for it and RTriangle keeps
it.

```{r compare, eval = has_rtriangle && has_decido && has_silicate}
tri_area <- function(P, T) {
  sum(abs((P[T[, 2], 1] - P[T[, 1], 1]) * (P[T[, 3], 2] - P[T[, 1], 2]) -
          (P[T[, 3], 1] - P[T[, 1], 1]) * (P[T[, 2], 2] - P[T[, 1], 2]))) / 2
}
iw_p <- as_pslg(iw)
iw_rt <- RTriangle::triangulate(
  RTriangle::pslg(P = iw_p$P, S = iw_p$S, H = pool_holes(iw))
)
ev <- pool_vertices(iw_tri)
c(
  earcut = tri_area(cbind(ev$x, ev$y), as.matrix(pool_triangles(iw_tri)[1:3])),
  RTriangle = tri_area(iw_rt$P, iw_rt$T)
) / 1e6
```

## The islands

The three islands, each drawn from its own subset of the triangle pool:

```{r islands, eval = has_decido && has_silicate, fig.height = 3.5}
op <- par(mfrow = c(1, 3), mar = c(0.5, 0.5, 2, 0.5))
for (nm in names(islands)) {
  plot(iw_tri[tt$.part == islands[nm]], border = "grey30", asp = 1,
       axes = FALSE, xlab = "", ylab = "", main = nm)
}
par(op)
```

A few things to notice:

* A subset is a set of triangles, not a copy of the coordinates: each
  island still references the full vertex pool, and its vertex ids are
  the ids in the whole coverage.

  ```{r islands-pool, eval = has_decido && has_silicate}
  mq <- iw_tri[tt$.part == islands["Macquarie Island"]]
  c(triangles = length(mq), pool_vertices = nrow(pool_vertices(mq)))
  ```

* Ear cutting adds no vertices, so the count is fixed by the input: a
  polygon with `n` vertices and `h` holes gives `n + 2h - 2` triangles.
  These islands have no holes, so each has two fewer triangles than
  vertices:

  ```{r islands-count, eval = has_decido && has_silicate}
  sapply(islands, function(k) {
    i <- tt$.part == k
    c(vertices = length(unique(unlist(tt[i, 1:3]))), triangles = sum(i))
  })
  ```

* The islands are a few kilometres to a few tens of kilometres across,
  the mainland states around a thousand. A maximum triangle area (`a` in
  RTriangle) is in map units, square metres here, so one value refines a
  small island and a large state very differently; a minimum angle (`q`)
  does not depend on scale.

## Which engine

| Engine | Adds vertices | Keeps feature | Holes from |
|--------|---------------|---------------|------------|
| `pool_triangulate()` (decido) | no | yes | ring roles |
| RTriangle via `as_pslg()` | yes (quality, area) | no | `pool_holes()` points |

[meshcore](https://github.com/hypertidy/meshcore) offers further
constrained Delaunay engines behind `tri_mesh()`, with vertex origin (input, crossing, Steiner) and depth
recorded in its mesh tables.


## Further reading

- [trianglewins](https://mdsumner.github.io/trianglewins/): a guide to
  constrained triangulation in R, with a survey of the packages, earcut
  and GEOS compared on the same inputs, and a harness that runs RTriangle
  and newer engines on shared test cases.
