# Copyright (C) 2017 Dr. Nikolai Knapp, UFZ
#
# This file is part of the slidaRtools R package.
#
# The slidaRtools R package is free software: you can redistribute
# it and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# slidaRtools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with slidaRtools If not, see <http://www.gnu.org/licenses/>.
#' Make a voxel forest from a tree list
#'
#' Function that creates a voxel forest and writes it to a file in XYZ-table format.
#' @param trees.dt A data.table (or data.frame) that contains all trees that form the voxelforest provided
#' with specific requirements: Each row represents one individual. The columns need to
#' have the following names and content: X = X-coordinate of tree position, Y = Y-coordinate
#' of tree position; H = tree height (m), CD = tree crown diameter (m), CL = tree crown length (m),
#' LAD = leaf area density of the tree (m2/m3), CS = tree crown shape (coding: 1 = cylinder,
#' 2 = spheroid, 3 = cone, 4 = icecone)
#' @param minx Minimum X-coordinate of the simulated area
#' @param maxx Maximum X-coordinate of the simulated area
#' @param miny Minimum Y-coordinate of the simulated area
#' @param maxy Maximum Y-coordinate of the simulated area
#' @param vxl.per.sqm Spatial resolution of the simulation in voxels per square meter
#' @param stems Boolean whether to include tree stems in the simulation or not
#' @param ground Boolean whether to include forest ground in the simulation or not
#' @param aggregation.func Function specifying how to deal with voxels in overlapping crowns
#' (that belong to more than one tree): Either the maximum, sum, mean or minimum of all
#' different values for that voxel can be taken, or if set to NA the values do not get
#' aggregated, thus multiple rows for that voxel are kept in the final voxelforest output.
#' Aggregation can only be performed if no additional columns should be kept, to avoid ambiguities.
#' If the parameter keep is set to something other than NA, it has priority and aggregation is
#' suppressed.
#' @param keep Optional vector of column names from which information should be kept
#' in each voxel of the output. E.g. choose keep=c("X", "Y", "TreeID") if stem positions and
#' ID of each tree should be available for each voxel. X and Y are special cases, because in
#' the output the columns "X" and "Y" specify the voxel coordinates. Thus, the columns
#' containing the stem coordinates are renamed to "Xstem" and "Ystem". Other kept columns
#' will appear in the output under their original input name.
#' @return data.table object containing a XYZ-table of tree crown voxels, ground voxels and maybe tree stem voxels
#' @keywords voxel forest plot stand lidar point cloud xyz
#' @import data.table
#' @export
#' @examples in progress
#' @author Nikolai Knapp
make_voxelforest <- function(trees.dt, minx, maxx, miny, maxy, vxl.per.sqm=4,
stems=F, ground=T, aggregation.func="max", keep=NA){
# # Settings for testing
# minx <- 100
# maxx <- 200
# miny <- 0
# maxy <- 100
# vxl.per.sqm=1
# stems=F
# ground=T
# aggregation.func="max"
# trees.dt <- data.table(sub.inv.dt[60,])
# Convert treelist to data.table
trees.dt <- data.table(trees.dt)
# Check with is.data.table (necessary for parLapply)
if(is.data.table(trees.dt) == T){
# Calculate resolution properties
vxl.sidelength <- 1/((vxl.per.sqm)^0.5)
vxl.per.m <- (vxl.per.sqm)^0.5
# Convert the coordinates of the treelist to fit the desired LiDAR resolution
trees.dt[, Xres := round(X * vxl.per.m)]
trees.dt[, Yres := round(Y * vxl.per.m)]
trees.dt[, CDres := round(CD * vxl.per.m)]
trees.dt[, CRres := ceiling(CDres/2)]
# Create an empty data.table to store the voxelized forest
voxelforest.dt <- data.table()
# Split the trees into crown shape classes
for(my.CS in unique(trees.dt$CS)){
#my.CS <- 1
sub.trees.dt <- subset(trees.dt, CS == my.CS)
if(nrow(sub.trees.dt) > 0){
# Repeat each row in the tree data.table as many times as there are voxels in the
# bounding box in Z direction
sub.trees.voxel.dt <- sub.trees.dt[rep(1:.N, ceiling(H+1))]
# Number the replicates
sub.trees.voxel.dt[, Zbox := 1:.N, by="TreeID"]
# Repeat each row in the tree data.table as many times as there are voxels in the
# bounding box in Y direction
sub.trees.voxel.dt <- sub.trees.voxel.dt[rep(1:.N, ceiling(CDres+5))]
# Number the replicates
sub.trees.voxel.dt[, Ybox := 1:.N, by=c("TreeID", "Zbox")]
# Repeat each row in the tree data.table as many times as there are voxels in the
# bounding box in X direction
sub.trees.voxel.dt <- sub.trees.voxel.dt[rep(1:.N, ceiling(CDres+5))]
# Number the replicates
sub.trees.voxel.dt[, Xbox := 1:.N, by=c("TreeID", "Zbox", "Ybox")]
# Shift the bounding boxes to the respective tree XY-positions
sub.trees.voxel.dt[, Xbox := Xbox + Xres - CRres - 1]
sub.trees.voxel.dt[, Ybox := Ybox + Yres - CRres - 1]
# # testing
# df <- data.frame(sub.trees.voxel.dt)
# df <- subset(df, select=c("Xbox", "Ybox", "Zbox"))
# names(df) <- c("X", "Y", "Z")
# display.point.cloud(df)
# Experiment with distance dependent LAD
# # Calculate the distance to the crown center
# sub.trees.voxel.dt$CtrDist <- ((sub.trees.voxel.dt$Xbox-sub.trees.voxel.dt$Xres)^2+(sub.trees.voxel.dt$Ybox-sub.trees.voxel.dt$Yres)^2+(sub.trees.voxel.dt$Zbox-(sub.trees.voxel.dt$H-sub.trees.voxel.dt$CL/2))^2)^(1/3)
# sub.trees.voxel.dt$LAD <- sub.trees.voxel.dt$LAD * sub.trees.voxel.dt$CtrDist
# Depending on the crown shape subset only voxels that fall into the crown
if(my.CS == 1){
sub.crown.voxel.dt <- subset(sub.trees.voxel.dt, in_cylinder(Xcor=sub.trees.voxel.dt$Xbox, Ycor=sub.trees.voxel.dt$Ybox, Zcor=sub.trees.voxel.dt$Zbox, Xctr=sub.trees.voxel.dt$Xres, Yctr=sub.trees.voxel.dt$Yres, Zbase=(sub.trees.voxel.dt$H-sub.trees.voxel.dt$CL), radius=sub.trees.voxel.dt$CRres, height=sub.trees.voxel.dt$CL) == 1)
} else if(my.CS == 2){
sub.crown.voxel.dt <- subset(sub.trees.voxel.dt, in_spheroid(Xcor=sub.trees.voxel.dt$Xbox, Ycor=sub.trees.voxel.dt$Ybox, Zcor=sub.trees.voxel.dt$Zbox, Xctr=sub.trees.voxel.dt$Xres, Yctr=sub.trees.voxel.dt$Yres, Zctr=(sub.trees.voxel.dt$H-sub.trees.voxel.dt$CL/2), radius=sub.trees.voxel.dt$CRres, height=sub.trees.voxel.dt$CL) == 1)
} else if(my.CS == 3){
sub.crown.voxel.dt <- subset(sub.trees.voxel.dt, in_cone(Xcor=sub.trees.voxel.dt$Xbox, Ycor=sub.trees.voxel.dt$Ybox, Zcor=sub.trees.voxel.dt$Zbox, Xctr=sub.trees.voxel.dt$Xres, Yctr=sub.trees.voxel.dt$Yres, Zbase=(sub.trees.voxel.dt$H-sub.trees.voxel.dt$CL), radius=sub.trees.voxel.dt$CRres, height=sub.trees.voxel.dt$CL) == 1)
} else if(my.CS == 4){
sub.crown.voxel.dt <- subset(sub.trees.voxel.dt, in_icecone(Xcor=sub.trees.voxel.dt$Xbox, Ycor=sub.trees.voxel.dt$Ybox, Zcor=sub.trees.voxel.dt$Zbox, Xctr=sub.trees.voxel.dt$Xres, Yctr=sub.trees.voxel.dt$Yres, Zctr=(sub.trees.voxel.dt$H-sub.trees.voxel.dt$CL/2), radius=sub.trees.voxel.dt$CRres, height=sub.trees.voxel.dt$CL) == 1)
}
# Collect results
voxelforest.dt <- rbind(voxelforest.dt, sub.crown.voxel.dt)
# Add tree stems
if(stems == T){
sub.stem.voxel.dt <- subset(sub.trees.voxel.dt, Xbox==Xres & Ybox==Yres & Zbox<(H-CL))
voxelforest.dt <- rbind(voxelforest.dt, sub.stem.voxel.dt)
}
}
}
# Add the forest floor using CJ which works like expand.grid, but in a fast data.table speed
if(ground == T){
ground.voxel.dt <- CJ(Xbox=(minx*vxl.per.m):(maxx*vxl.per.m), Ybox=(miny*vxl.per.m):(maxy*vxl.per.m), Zbox=0, LAD=1)
voxelforest.dt <- rbind(voxelforest.dt, ground.voxel.dt, fill=T)
}
# Check if certain columns should be kept
if(length(keep) == 1){
if(is.na(keep)){
# Subset only the relevant columns
voxelforest.dt <- subset(voxelforest.dt, select=c("Xbox", "Ybox", "Zbox", "LAD"))
setnames(voxelforest.dt, c("X", "Y", "Z", "LAD"))
voxelforest.dt$LAD <- round(voxelforest.dt$LAD, 2)
}
} else {
voxelforest.dt <- subset(voxelforest.dt, select=c("Xbox", "Ybox", "Zbox", "LAD", keep))
keep[which(keep == "X")] <- "Xstem"
keep[which(keep == "Y")] <- "Ystem"
setnames(voxelforest.dt, c("X", "Y", "Z", "LAD", keep))
voxelforest.dt$LAD <- round(voxelforest.dt$LAD, 2)
}
# Aggregate redundant points (i.e. voxels in crown overlaps).
# The syntax from the data.table package takes the statistic from the column "value", grouped by unique
# combinations of columns "X", "Y" and "Z" and writes it to a column "value" in the output.
# Aggregation can only be performed if no additional columns should be kept, to avoid ambiguities.
if(length(keep) == 1){
if(is.na(keep)){
if(aggregation.func == "max"){
voxelforest.dt <- voxelforest.dt[,.(LAD=max(LAD)), by='X,Y,Z']
} else if(aggregation.func == "mean"){
voxelforest.dt <- voxelforest.dt[,.(LAD=mean(LAD)), by='X,Y,Z']
} else if(aggregation.func == "min"){
voxelforest.dt <- voxelforest.dt[,.(LAD=min(LAD)), by='X,Y,Z']
} else if(aggregation.func == "sum"){
voxelforest.dt <- voxelforest.dt[,.(LAD=sum(LAD)), by='X,Y,Z']
}
}
}
# Convert LiDAR density skewed coordinates to real coordinates in meters
voxelforest.dt[, X := round(X * vxl.sidelength, 2)]
voxelforest.dt[, Y := round(Y * vxl.sidelength, 2)]
# Sort the dataframe in the order X-, Y- and Z-coordinate
# with the crown layers in decreasing order
setorderv(voxelforest.dt, c("X", "Y", "Z"), c(1, 1, -1))
# Calculate cumulative leaf area index for each voxel
voxelforest.dt[, LAI := cumsum(LAD), by=c("X", "Y")]
return(voxelforest.dt)
}
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.