|
| 1 | + |
| 2 | + |
| 3 | +get_range <- function(x, sp, land, include=10, exclude=100) { |
| 4 | + |
| 5 | + stopifnot(include <= exclude) |
| 6 | + |
| 7 | + # km to m |
| 8 | + CAmin <- include * 1000 |
| 9 | + CAmax <- exclude * 1000 |
| 10 | + |
| 11 | + if ((CAmax > 0) && (CAmax < Inf)) { |
| 12 | + ca_remove <- terra::buffer(sp, CAmax) |
| 13 | + x <- terra::mask(x, ca_remove, updatevalue=NA) |
| 14 | + } |
| 15 | + if (CAmin > 0) { |
| 16 | + ca_add <- terra::buffer(sp, CAmin, quadsegs=12) |
| 17 | + x <- terra::rasterize(ca_add, x, update=TRUE) |
| 18 | + } |
| 19 | + if (!is.null(land)) { |
| 20 | + x <- terra::mask(x, land) |
| 21 | + } |
| 22 | + # terra::ifel(x==1, 1, NA) |
| 23 | + x |
| 24 | +} |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +get_EnvDist <- function(env, regions, envfun) { |
| 29 | + e <- terra::extract(env, regions, fun=mean, na.rm=TRUE, ID=FALSE) |
| 30 | + |
| 31 | + dst <- lapply(1:ncol(e), \(i) stats::dist(e[,i])) |
| 32 | + x <- do.call(data.frame, dst) |
| 33 | + names(x) <- names(env) |
| 34 | + # express environmental distance expressed as geographic distance |
| 35 | + envd <- envfun(x) |
| 36 | + structure(envd, class = 'dist', Size=attr(dst[[1]], "Size")) |
| 37 | +} |
| 38 | + |
| 39 | + |
| 40 | +.n_zones <- function(x, min_area, m=3) { |
| 41 | + round(pmax(1, pmin(x, m*sqrt(x)))) |
| 42 | +} |
| 43 | + |
| 44 | + |
| 45 | +make_zones <- function(x, range, n, spread=TRUE) { |
| 46 | + |
| 47 | + if (spread) { # spread sample across polygons |
| 48 | + drange <- terra::disagg(range) |
| 49 | + rr <- terra::rasterize(drange, terra::rast(x), 1:nrow(drange)) |
| 50 | + p <- terra::as.polygons(rr) |
| 51 | + p$area <- terra::expanse(p, "km") / 1000 |
| 52 | + avga <- sum(p$area) / n |
| 53 | + p$n <- round(p$area / avga) |
| 54 | + totn <- sum(p$n) |
| 55 | + while (totn > n) { |
| 56 | + p$error <- p$area - p$n * avga |
| 57 | + i <- which.max(p$error) |
| 58 | + p$n[i] <- p$n[i] - 1 |
| 59 | + totn <- sum(p$n) |
| 60 | + } |
| 61 | + while (totn < n) { |
| 62 | + p$error <- p$area - p$n * avga |
| 63 | + i <- which.min(p$error) |
| 64 | + p$n[i] <- p$n[i] + 1 |
| 65 | + totn <- sum(p$n) |
| 66 | + } |
| 67 | + p <- p[p$n > 0, ] |
| 68 | + seeds <- lapply(1:nrow(p), function(i) { |
| 69 | + out <- terra::spatSample(p[i,], p$n[i]) |
| 70 | + if (nrow(out) < p$n[i]) { |
| 71 | + out <- terra::spatSample(p[i,], p$n[i] * 10) |
| 72 | + out <- out[sample(nrow(out), p$n[i]), ] |
| 73 | + } |
| 74 | + out |
| 75 | + }) |
| 76 | + |
| 77 | + seeds <- terra::crds(terra::vect(seeds)) |
| 78 | + |
| 79 | + km <- terra::k_means(terra::mask(x, rr), seeds, iter.max = 25) |
| 80 | + } else { |
| 81 | + |
| 82 | + xm <- terra::mask(x, range) |
| 83 | + km <- terra::k_means(xm, n, iter.max = 25) |
| 84 | + } |
| 85 | + |
| 86 | + terra::as.polygons(km) |
| 87 | +} |
| 88 | + |
| 89 | +get_samplesize <- function(x, fun=NULL, ...) { |
| 90 | + if (inherits(x, "SpatRaster")) { |
| 91 | + x <- terra::as.polygons(x) |
| 92 | + x <- x[x[,1,drop=TRUE]==1] |
| 93 | + } else if (!inherits(x, "SpatVector")) { |
| 94 | + stop("x should be a SpatVector") |
| 95 | + } |
| 96 | + a <- sum(terra::expanse(x, unit="km")) |
| 97 | + if (is.null(fun)) { |
| 98 | + fun <- function(A, omega) max(1, round(omega * sqrt(A/pi))) |
| 99 | + } |
| 100 | + n <- fun(a, ...) |
| 101 | + #n <- max(nmin, n) |
| 102 | + #z <- max(1, min(n, round(a/min_area))) |
| 103 | + return(list(range=x, area=a, n=n)) |
| 104 | +} |
| 105 | + |
| 106 | + |
| 107 | + |
| 108 | +small_ssize_penalty <- function(ssize, minssize) { |
| 109 | + ifelse(ssize > minssize[1], 1, ssize / minssize[1]) |
| 110 | +} |
| 111 | + |
| 112 | + |
| 113 | + |
| 114 | +get_network <- function(regions, sample, maxdist=1500) { |
| 115 | + |
| 116 | + terra::values(regions) <- data.frame(id=1:nrow(regions)) |
| 117 | + patches <- terra::disagg(terra::aggregate(regions)) |
| 118 | + patches$pid <- 1:nrow(patches) # pid = patch id |
| 119 | + |
| 120 | + x <- terra::centroids(regions, inside=TRUE) |
| 121 | + |
| 122 | + x$pid <- terra::extract(patches, x)$pid |
| 123 | + patches <- patches[unique(x$pid), ] |
| 124 | + np <- nrow(patches) |
| 125 | + |
| 126 | + x <- terra::round(x, 5) # to help merge |
| 127 | + xy <- terra::crds(x) |
| 128 | + |
| 129 | + adj <- terra::adjacent(regions) |
| 130 | + adj <- data.frame(unique(t(apply(adj, 1, sort)))) |
| 131 | + if (ncol(adj) == 0) { |
| 132 | + adj <- data.frame(from=NA, to=NA, adj=NA) |
| 133 | + adj <- adj[0,] |
| 134 | + } else { |
| 135 | + colnames(adj) <- c("from", "to") |
| 136 | + adj$adj <- 1 |
| 137 | + } |
| 138 | + if (np > 1) { |
| 139 | + up <- sort(unique(x$pid)) |
| 140 | + dx <- as.matrix(terra::distance(x)) |
| 141 | + diag(dx) <- NA |
| 142 | + i <- match(colnames(dx), x$id) |
| 143 | + pid <- x$pid[i] |
| 144 | + for (p in up) { |
| 145 | + s <- dx[pid != p, pid == p, drop=FALSE] |
| 146 | + rid <- pid[pid != p] |
| 147 | + upp <- sort(unique(rid)) |
| 148 | + for (pp in upp) { |
| 149 | + ss <- s[rid == pp, ,drop=FALSE] |
| 150 | + j <- which.min(apply(ss, 1, min)) |
| 151 | + k <- which.min(ss[j, ]) |
| 152 | + add <- c(sort(as.integer(c(rownames(ss)[j], colnames(ss)[k]))), 0) |
| 153 | + add <- data.frame(from=add[1], to=add[2], adj=add[3]) |
| 154 | + adj <- rbind(adj, add) |
| 155 | + } |
| 156 | + } |
| 157 | + adj <- unique(adj) |
| 158 | + } |
| 159 | + |
| 160 | + colnames(xy) <- c("xf", "yf") |
| 161 | + adj <- cbind(adj, xy[adj$from, , drop=FALSE]) |
| 162 | + colnames(xy) <- c("xt", "yt") |
| 163 | + adj <- cbind(adj, xy[adj$to, , drop=FALSE]) |
| 164 | + |
| 165 | + adj <- cbind(adj, w=1, dst=terra::distance(x[adj$from, ], x[adj$to, ], pairwise=TRUE, unit="m")/1000) |
| 166 | + |
| 167 | + if (np > 2) { |
| 168 | + padj <- adj[adj$adj == 0, ] |
| 169 | + adj <- adj[adj$adj != 0, ] |
| 170 | + nx <- nrow(x) |
| 171 | + padj <- padj[order(padj$dst), ] |
| 172 | + g <- igraph::components( igraph::graph_from_data_frame(adj, directed = FALSE) ) |
| 173 | + for (i in 1:nrow(padj)) { |
| 174 | + adj2 <- rbind(adj, padj[i,]) |
| 175 | + gg <- igraph::components( igraph::graph_from_data_frame(adj2, directed = FALSE) ) |
| 176 | + if ((gg$no < g$no) | (length(gg$membership) > length(g$membership))) { |
| 177 | + g <- gg |
| 178 | + adj <- adj2 |
| 179 | + } |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | +# mxd <- median(adj$dst) * 3 |
| 184 | +# adj$dst[adj$dst > mxd]] <- mxd |
| 185 | + |
| 186 | + adj$dst[adj$dst > maxdist] <- maxdist |
| 187 | + adj |
| 188 | +} |
| 189 | + |
| 190 | + |
| 191 | +dst2svect <- function(x) { |
| 192 | + m <- as.matrix(x[, c("xf", "yf", "xt", "yt")]) |
| 193 | + a <- lapply(1:nrow(m), \(i) cbind(i, matrix(m[i,], nrow=2, byrow=2))) |
| 194 | + b <- do.call(rbind, a) |
| 195 | + v <- terra::vect(b, "lines", crs="lonlat") |
| 196 | + terra::values(v) <- x |
| 197 | + v |
| 198 | +} |
| 199 | + |
| 200 | +dst2graph <- function(x) { |
| 201 | + gg <- igraph::graph_from_data_frame(x, directed = FALSE) |
| 202 | + igraph::E(gg)$weight <- x$dst * x$w |
| 203 | + gg |
| 204 | +} |
| 205 | + |
| 206 | + |
| 207 | +add2rr <- function(rr, pair) { |
| 208 | + add <- rr[0,] |
| 209 | + add[1,1:2] <- as.integer(pair) |
| 210 | + j <- which(rr$from == add[1,1])[1] |
| 211 | + if (is.na(j)) { |
| 212 | + j <- which(rr$to == add[1,1])[1] |
| 213 | + if (!is.na(j)) { |
| 214 | + add[, c("xf", "yf")] <- rr[j, c("xt", "yt")] |
| 215 | + } |
| 216 | + } else { |
| 217 | + add[, c("xf", "yf")] <- rr[j, c("xf", "yf")] |
| 218 | + } |
| 219 | + j <- which(rr$from == add[1,2])[1] |
| 220 | + if (is.na(j)) { |
| 221 | + j <- which(rr$to == add[1,2])[1] |
| 222 | + if (!is.na(j)) { |
| 223 | + add[, c("xt", "yt")] <- rr[j, c("xt", "yt")] |
| 224 | + } |
| 225 | + } else { |
| 226 | + add[, c("xt", "yt")] <- rr[j, c("xf", "yf")] |
| 227 | + } |
| 228 | + add$w <- add$dst <- 0 |
| 229 | + rbind(rr, add) |
| 230 | +} |
| 231 | + |
| 232 | +XC <- function(regions, seed, env=NULL, envfun=NULL, minssize=10, maxdist=1500) { |
| 233 | + |
| 234 | +## TODO RH |
| 235 | +# refine the adjust effect such that when you have many observations in one zones |
| 236 | +# they can only contribute to their neighbors. Do not increase branch length to avoid that |
| 237 | +# one region does not compensate for another |
| 238 | + |
| 239 | + rr <- get_network(regions, seed, maxdist) |
| 240 | + |
| 241 | + if (!is.null(env)) { |
| 242 | + envd <- as.matrix(get_EnvDist(env, regions, envfun)) |
| 243 | + rr$envdst <- envd[as.matrix(rr[,1:2])] |
| 244 | + rr$geodst <- rr$dst |
| 245 | + # sum geo and env dist |
| 246 | + rr$dst <- rr$dst + rr$envdst |
| 247 | + } |
| 248 | + gg <- igraph::graph_from_data_frame(rr, directed = FALSE) |
| 249 | + igraph::E(gg)$weight <- rr$dst * rr$w |
| 250 | + y <- unique(terra::extract(regions, seed)[,2]) |
| 251 | + rr$w <- rowSums(!matrix(as.matrix(rr[,1:2]) %in% y, ncol=2)) / 2 |
| 252 | + igraph::E(gg)$weight2 <- rr$dst * rr$w |
| 253 | + |
| 254 | + if (nrow(seed) <= 0) { |
| 255 | + rr$weight <- rr$w * rr$dst |
| 256 | + return(list(XC=0, dist=rr)) |
| 257 | + } |
| 258 | + |
| 259 | + |
| 260 | + n <- igraph::count_components(gg) |
| 261 | + score <- nodes <- rep(NA, n) |
| 262 | + dg <- igraph::decompose(gg) |
| 263 | + for (k in 1:n) { |
| 264 | + g <- dg[[k]] |
| 265 | + dst <- igraph::distances(g) |
| 266 | + d1 <- sum(dst) |
| 267 | + |
| 268 | + igraph::E(g)$weight <- igraph::E(g)$weight2 |
| 269 | + |
| 270 | + if (length(y) > 1) { |
| 271 | + b <- utils::combn(as.character(y), 2) |
| 272 | + nms <- igraph::V(g)$name |
| 273 | + haveb <- apply(matrix(b %in% nms, nrow=2), 2, all) |
| 274 | + b <- b[,haveb,drop=FALSE] |
| 275 | + if (ncol(b) > 0) { |
| 276 | + for (i in 1:ncol(b)) { |
| 277 | + if (!igraph::are_adjacent(g, b[1,i], b[2,i])) { |
| 278 | + g <- igraph::add_edges(g, b[,i], weight=0) |
| 279 | + rr <- add2rr(rr, b[,i]) |
| 280 | + } |
| 281 | + } |
| 282 | + } |
| 283 | + } |
| 284 | + d2 <- igraph::distances(g) |
| 285 | + score[k] <- 1 - (sum(d2) / d1) |
| 286 | + nodes[k] <- length(g) |
| 287 | + } |
| 288 | + score <- stats::weighted.mean(score, nodes) |
| 289 | + |
| 290 | + if (minssize > 0) { |
| 291 | + score <- score * small_ssize_penalty(length(seed), minssize) |
| 292 | + } |
| 293 | + |
| 294 | + rr$weight <- rr$w * rr$dst |
| 295 | + list(XC=score, dist=rr) |
| 296 | +} |
| 297 | + |
0 commit comments