R/nightlight_calculate.R

Defines functions nightlight_calculate

Documented in nightlight_calculate

#' nightlight_calculate
#'
#' Perform calculations on nightlights of a region in a given time interval
#' (defaults are sum of light values, min, mean and max, plus mean_obs, which
#' indicates the mean number of observations (with respect to pixels) that went
#' into the aggregated image for the time period in a given area).
#'
#' @param area_names May not be empty. String (vector) with the name(s) of your
#' region(s). If you provide a country name, the country shapefile will be
#' downloaded automatically. Shapefiles in your shapefile location that have
#' the area_names or (in case they exist) their iso3c countrycodes in their
#' filename will be detected automatically. If you provide own shapefiles in
#' the shapefiles argument, put the names in order of the shapefile order. If
#' you only put one name for multiple shapefiles, all shapefiles will be
#' processed with that name.
#' @param time May not be empty. Vector of strings with the start/end dates in
#' the format "2012-04" (monthly data) or "1992" (yearly data). If only one
#' time period is desired, then simply input one date.
#' @param light_location May not be empty. Provide the location of the
#' lightfiles on your drive as a string. Note about the light files: for the
#' DMSP images, some years have 2 versions available. Generally, if possible,
#' a consistent version for your timespan will be chosen. Otherwise the newest
#' version for each year will be selected. If 2 consistent versions are
#' available for your timespan, the newer one will be selected.
#' @param shapefile_location May be empty. Provide the location of the
#' shapefiles on your drive as a string in the case that you want to use
#' shapefiles and not a set of coordinates.
#' @param shapefiles May be empty. You can provide own shapefiles here
#' (input are the filenames) if no automatic download is desired. If there is
#' a shapefile in your shapefile location that has either the name of the
#' region which you enter in area_names or the iso3c countrycode
#' (if it is a country) in its filename, it will be detected automatically and
#' you do not have to use this argument.
#' @param download_shape Default is ".gpkg", alternative is ".shp". Will
#' only download if no own shapefiles are provided in the shapefiles argument
#' or automatically detected in the shapefile location.
#' @param gpkg_layer May be empty. You might need this argument if the code
#' does not detect the correct layer of a .gpkg file automatically. This can
#' happen if the layers of your .gpkg shapefile do not include an admlevel in
#' their names. In that case, enter the layer here as a string. Note that this
#' only works for one area at a time. To find out which layers are included in
#' your .gpkg shapefile, you can use sf::st_layers().
#' @param admlevel Default is 0. Change this when working with different
#' administrative levels. Important for nightlight_calculate: if your shapefile
#' is not from GADM and features adm levels larger than 0, you must rename the
#' column names to NAME_X, X being the admlevel. You can do this by reading
#' the shapefile, renaming the columns of the SpatialPolygonsDataFrame accordingly 
#' and then saving the shapefile again. With this naming convention, the calculation 
#' with lower admlevels will work if the shapefile is not from GADM, otherwise,
#' this is not guaranteed.
#' @param single_dataframes Default is FALSE. If set to TRUE, this will not
#' only output a dataframe with all areas into the global environment, but also
#' a dataframe for each area specified in area_names.
#' @param functions_calculate Defaults (activated if this is NULL) are sum of
#' light values, min, mean, and max. These calculations will be performed on
#' the night light data for your given time and region. Functions need to
#' support the possibility to set na.rm as an argument. See the documentation
#' of raster::extent(), into which the functions are fed, for further details
#' if you encounter problems. The functions have to be given in a vector of
#' strings, since they are called with get(). The name of the functions will
#' be the column name in the final dataframe output as well.
#' @param rectangle_calculate Default is NULL, which leads to an automatic
#' detection whether your shapefile has to be transformed to a rectangle or not.
#' You can set this to TRUE if you encounter problems with your shapefile -
#' the code will calculate the values for the smallest rectangle that reaches
#' over the full extent of the shapefile. The code will set this automatically
#' to TRUE if a shapefile is not of class SpatialPolygons or if it is of class
#' SpatialPolygons but does not allow feature a unique enclosed area.
#' You can also set this manually to FALSE if for some reason the code wrongly
#' identifies your area as non-enclosed.
#' @param rawdata Default is FALSE. If set to TRUE, an additional file with the
#' raw night light data, i.e. the light values and their coordinates, will be
#' produced for each time period.
#' @param cut_low May be empty. If a value is provided, light values below
#' cut_low will be set to NA before performing calculations.
#' @param cut_high May be empty. If a value is provided, light values above
#' cut_high will be set to NA before performing calculations.
#' @param cut_quality Default is 0. This will set all light values to NA
#' for which the number of observations that went into the calculation of a
#' pixel is smaller than or equal to cut_quality. Hence, with the default
#' value you will have all pixels set to NA for which 0 observations were
#' made. You can set this to a higher number if you want to drop all pixels
#' below a given number of observations.
#' @param user_coordinates May be empty. Inputs are decimal numbers (longlat).
#' Can be specified if you want to calculate night lights for a region
#' defined by specific coordinates. Input order is xmin, xmax, ymin, ymax.
#' @param corrected_lights Default is FALSE. If set to TRUE, the
#' radiance-calibrated version of the DMSP data or the straylight-corrected
#' version of the VIIRS data will be used. Note that you have to download these
#' versions first, since they are different images from the standard ones. The data
#' refer to the corrected lights by Bluhm and Krause (2022).
#' @param harmonized_lights Default is FALSE. If set to TRUE, the harmonized
#' DMSP-VIIRS yearly dataset by Li et al. (2020) will be used. Note that you
#' have to download these versions first, since they are different images from
#' the standard ones.
#' @export

nightlight_calculate <- function(area_names,
                                 time,
                                 light_location,
                                 shapefile_location = NULL,
                                 shapefiles = NULL,
                                 download_shape = ".gpkg",
                                 gpkg_layer = NULL,
                                 admlevel = 0,
                                 single_dataframes = FALSE,
                                 functions_calculate = NULL,
                                 rectangle_calculate = NULL,
                                 rawdata = FALSE,
                                 cut_low = NULL,
                                 cut_high = NULL,
                                 cut_quality = 0,
                                 user_coordinates = NULL,
                                 corrected_lights = FALSE,
                                 harmonized_lights = FALSE){
  
  help_shapefiles <- area_name <- lightdata_time <- tilenumbers <- ISO3 <-
    ISO3s <- dmsp_consistent <- dmsp_stump <- skip_period <- extent <- year <-
    month <- NULL # set the variables to NULL first to bind variables so
  # there is no "binding for global variables" problem later
  
  if (is.null(functions_calculate)){
    functions_calculate <- c("sum", "min", "mean", "max")
  }
  
  if (is.null(shapefile_location) & is.null(user_coordinates)){
    stop(paste0("Please input geographical information, either by providing ",
                "shapefiles (do not forget to use shapefile_location to ",
                "specify the location on your drive) or a set of coordinates."))
  }
  
  source_setup(light_location = light_location,
               shapefile_location = shapefile_location,
               time = time, area_names = area_names,
               shapefiles = shapefiles)
  
  
  for (i in 1:length(area_names)){
    
    area_name <- area_names[i]
    ISO3 <- ISO3s[i]
    
    if (is.na(ISO3)){
      print(paste0("An iso3c countrycode for ", area_name, " could not be ",
                   "found, hence the download from GADM will not work. Either ",
                   "your shapefile is not a country or, if it is a country, ",
                   "the countryname was not recognized correctly."))
    }
    
    get_shapefile(i = i,
                  area_name = area_name,
                  ISO3 = ISO3,
                  help_shapefiles = help_shapefiles,
                  shapefiles = shapefiles,
                  admlevel = admlevel,
                  gpkg_layer = gpkg_layer,
                  user_coordinates = user_coordinates,
                  shapefile_location = shapefile_location,
                  download_shape = download_shape,
                  lightdata_time = lightdata_time)
    
    # in case user did not set rectangle to TRUE: check for enclosed area
    # and set automatically
    if (is.null(rectangle_calculate)){
      rectangle_manual_setting <- FALSE
      if (admlevel != 0){
        rectangle_calculate <- FALSE # for lower admlevels, the code below
        # will detect rectangle_calculate = TRUE because there are multiple
        # areas. however, a lower-admlevel shapefile basically has to be
        # enclosed, so set to FALSE. if desired, the user can still override it
      } else if (admlevel == 0){
        # there are some uncommon formats that could be rectangularized, even though
        # they should not; the user will get a message about the rectangularization 
        # and can set rectangle_calculate to FALSE manually if necessary
        help_area <- suppressWarnings(sf::st_area(shapefile))
        if (length(help_area) == 1){
          if (!is.na(help_area)){
            rectangle_calculate <- FALSE
          } else if (is.na(help_area)){
            rectangle_calculate <- TRUE
          }
        } else if (length(help_area) != 1){
          rectangle_calculate <- TRUE
        }
      }
    } else if (!is.null(rectangle_calculate)){
      rectangle_manual_setting <- TRUE
    }
    
    # transform non-enclosed shapefile into a rectangle if
    # rectangle_calculate is TRUE
    if (rectangle_calculate == TRUE &
        rectangle_manual_setting == FALSE &
        is.null(user_coordinates)){
      crs <- suppressWarnings(raster::crs(shapefile))
      shapefile <- methods::as(extent, "SpatialPolygons")
      raster::crs(shapefile) <- crs
      shapefile <- sp::SpatialPolygonsDataFrame(shapefile,
                                                data.frame(N = c("1"),
                                                           row.names = c("1")))
      shapefile <- sf::st_as_sf(shapefile)
      print(paste0("The shapefile for ", area_name , " features a ",
                   "non-enclosed area. The calculations will be performed on a ",
                   "rectangular version of the shapefile defined by its minimum and ",
                   "maximum extent."))
    }
    
    # at this point we have either an enclosed shapefile or a rectangle.
    # hence, an area can be calculated. for lower admlevels, area will output
    # as many values as there are regions of the lowest admlevel
    area <- as.numeric((suppressWarnings(sf::st_area(shapefile))) / (1000^2))
    # area is output in sq meters; convert to sq kilometers
    
    
    if (lightdata_time == "monthly"){
      all_aggregated <- data.frame(area_name = "",
                                   iso3c = "",
                                   area_km2 = 0,
                                   yearmonth = "")
    } else if (lightdata_time == "yearly"){
      all_aggregated <- data.frame(area_name = "",
                                   iso3c = "",
                                   area_km2 = 0,
                                   year = 0)
    }
    
    if (harmonized_lights == FALSE){
      all_aggregated$mean_obs = 0
    }
    
    if (!is.null(shapefile$NAME_1)){
      all_aggregated$NAME_1 <- ""
    }
    
    if (!is.null(shapefile$NAME_2)){
      all_aggregated$NAME_2 <- ""
    }
    
    if (!is.null(shapefile$NAME_3)){
      all_aggregated$NAME_3 <- ""
    }
    
    if (!is.null(shapefile$NAME_4)){
      all_aggregated$NAME_4 <- ""
    }
    
    if (!is.null(shapefile$NAME_5)){
      all_aggregated$NAME_5 <- ""
    }
    
    for (k in 1:length(functions_calculate)){
      current_function_name <- functions_calculate[k]
      name_df <- data.frame(current_function_name = 0)
      colnames(name_df)[colnames(name_df) ==
                          "current_function_name"] <- current_function_name
      all_aggregated <- cbind(all_aggregated, name_df)
    }
    
    all_aggregated <- all_aggregated[-c(1),]
    
    for (j in 1:length(sequence)){
      
      get_lightfile(j = j,
                    light_location = light_location,
                    lightdata_time = lightdata_time,
                    sequence = sequence,
                    tilenumbers = tilenumbers,
                    corrected_lights = corrected_lights,
                    harmonized_lights = harmonized_lights,
                    extent = extent)
      
      if (skip_period == TRUE){
        next
      }
      
      if (rawdata == TRUE){
        lightdata_uncut <- lightdata
        if (harmonized_lights == FALSE){
          qualitydata_uncut <- qualitydata
        }
      }
      
      if (harmonized_lights == FALSE){
        lightdata[qualitydata <= cut_quality] <- NA
        qualitydata[qualitydata <= cut_quality] <- NA
        # also set quality to NA so later the mean of observations per pixel is
        # calculated only for pixels above the cut_quality threshold
      }
      
      if (!is.null(cut_low)){
        lightdata[lightdata < cut_low] <- NA
        if (harmonized_lights == FALSE){
          qualitydata[lightdata < cut_low] <- NA
        }
      }
      
      if (!is.null(cut_high)){
        lightdata[lightdata > cut_high] <- NA
        if (harmonized_lights == FALSE){
          qualitydata[lightdata > cut_high] <- NA
        }
      }
      
      if (lightdata_time == "monthly"){
        aggregated <- data.frame(area_name = "",
                                 iso3c = "",
                                 area_km2 = 0,
                                 yearmonth = "")
      } else if (lightdata_time == "yearly"){
        aggregated <- data.frame(area_name = "",
                                 iso3c = "",
                                 area_km2 = 0,
                                 year = 0)
      }
      
      aggregated <- aggregated[-c(1),]
      
      # set the number of rows for the output dataframe according to the number
      # of the lowest-order subregional division (5 divisions are the most
      # finely divided divisions)
      if (!is.null(shapefile$NAME_5)){
        aggregated[nrow(aggregated) + length(shapefile$NAME_5),] <- NA
      } else if (is.null(shapefile$NAME_5) &
                 !is.null(shapefile$NAME_4)){
        aggregated[nrow(aggregated) + length(shapefile$NAME_4),] <- NA
      } else if (is.null(shapefile$NAME_5) &
                 is.null(shapefile$NAME_4) &
                 !is.null(shapefile$NAME_3)){
        aggregated[nrow(aggregated) + length(shapefile$NAME_3),] <- NA
      } else if (is.null(shapefile$NAME_5) &
                 is.null(shapefile$NAME_4) &
                 is.null(shapefile$NAME_3) &
                 !is.null(shapefile$NAME_2)){
        aggregated[nrow(aggregated) + length(shapefile$NAME_2),] <- NA
      } else if (is.null(shapefile$NAME_5) &
                 is.null(shapefile$NAME_4) &
                 is.null(shapefile$NAME_3) &
                 is.null(shapefile$NAME_2) &
                 !is.null(shapefile$NAME_1)){
        aggregated[nrow(aggregated) + length(shapefile$NAME_1),] <- NA
      } else if (is.null(shapefile$NAME_5) &
                 is.null(shapefile$NAME_4) &
                 is.null(shapefile$NAME_3) &
                 is.null(shapefile$NAME_2) &
                 is.null(shapefile$NAME_1)){
        aggregated[nrow(aggregated) + 1,] <- NA
      }
      
      if (!is.null(shapefile$NAME_5)){
        aggregated$NAME_5 <- shapefile$NAME_5
      }
      
      if (!is.null(shapefile$NAME_4)){
        aggregated$NAME_4 <- shapefile$NAME_4
      }
      
      if (!is.null(shapefile$NAME_3)){
        aggregated$NAME_3 <- shapefile$NAME_3
      }
      
      if (!is.null(shapefile$NAME_2)){
        aggregated$NAME_2 <- shapefile$NAME_2
      }
      
      if (!is.null(shapefile$NAME_1)){
        aggregated$NAME_1 <- shapefile$NAME_1
      }
      
      aggregated$area_name <- area_name
      aggregated$iso3c <- ISO3
      aggregated$area_km2 <- area
      if (lightdata_time == "monthly"){
        aggregated$yearmonth <- paste0(year, "-", month)
      } else if (lightdata_time == "yearly"){
        aggregated$year <- as.numeric(year)
      }
      
      if (harmonized_lights == FALSE){
        aggregated$mean_obs <- suppressWarnings(raster::extract(qualitydata,
                                                                shapefile,
                                                                fun = mean,
                                                                na.rm = TRUE))
      }
      
      for (k in 1:length(functions_calculate)){
        current_function_name <- functions_calculate[k]
        function_df <- data.frame(
          current_function_name =
            suppressWarnings(raster::extract(lightdata,
                                             shapefile,
                                             fun = get(
                                               functions_calculate[[k]]),
                                             na.rm = TRUE)))
        colnames(function_df)[colnames(
          function_df) == "current_function_name"] <- functions_calculate[k]
        aggregated <- cbind(aggregated, function_df)
      }
      
      all_aggregated <- rbind(all_aggregated, aggregated)
      
      if (rawdata == TRUE){
        
        rawdata_list <- suppressWarnings(
          raster::extract(
            lightdata_uncut,
            shapefile,
            cellnumbers = TRUE))
        
        rawdata_values <- data.frame(cell = 0, value = 0)
        
        if (!is.null(shapefile$NAME_5)){
          rawdata_values$NAME_5 <- ""
        }
        
        if (!is.null(shapefile$NAME_4)){
          rawdata_values$NAME_4 <- ""
        }
        
        if (!is.null(shapefile$NAME_3)){
          rawdata_values$NAME_3 <- ""
        }
        
        if (!is.null(shapefile$NAME_2)){
          rawdata_values$NAME_2 <- ""
        }
        
        if (!is.null(shapefile$NAME_1)){
          rawdata_values$NAME_1 <- ""
        }
        
        rawdata_values <- rawdata_values[-c(1),]
        
        for (l in 1:length(rawdata_list)){
          current_df <- data.frame(rawdata_list[[l]])
          
          if (!is.null(shapefile$NAME_5)){
            current_df$NAME_5 <- shapefile@data[["NAME_5"]][l]
          }
          
          if (!is.null(shapefile$NAME_4)){
            current_df$NAME_4 <- shapefile@data[["NAME_4"]][l]
          }
          
          if (!is.null(shapefile$NAME_3)){
            current_df$NAME_3 <- shapefile@data[["NAME_3"]][l]
          }
          
          if (!is.null(shapefile$NAME_2)){
            current_df$NAME_2 <- shapefile@data[["NAME_2"]][l]
          }
          
          if (!is.null(shapefile$NAME_1)){
            current_df$NAME_1 <- shapefile@data[["NAME_1"]][l]
          }
          rawdata_values <- rbind(rawdata_values, current_df)
        }
        rawdata_values <- cbind(
          rawdata_values,
          raster::coordinates(lightdata_uncut)[rawdata_values[,1],]
        )
        colnames(rawdata_values)[colnames(rawdata_values) ==
                                   "cell"] <- "cellnumber"
        colnames(rawdata_values)[colnames(rawdata_values) ==
                                   "value"] <- "lightvalue"
        
        if (harmonized_lights == FALSE){
          rawdata_quality_list <- suppressWarnings(raster::extract(
            qualitydata_uncut, shapefile, cellnumbers = TRUE))
          rawdata_quality <- data.frame(cell = 0, value = 0)[-c(1),]
          for (l in 1:length(rawdata_quality_list)){
            rawdata_quality <- rbind(rawdata_quality,
                                     data.frame(rawdata_quality_list[[l]]))
          }
          
          rawdata_values <- cbind(rawdata_values, rawdata_quality$value)
          colnames(rawdata_values)[colnames(rawdata_values) ==
                                     "rawdata_quality$value"] <- "number_obs"
        }
        if (lightdata_time == "monthly"){
          rawdata_name <- paste0("rawlights_",
                                 area_name, "_",
                                 year, "-",
                                 month)
        } else if (lightdata_time == "yearly"){
          rawdata_name <- paste0("rawlights_",
                                 area_name, "_",
                                 year)
        }
        assign(rawdata_name, rawdata_values, envir = .GlobalEnv)
      }
      
      if (single_dataframes == TRUE){
        # if user sets this to TRUE, the output will not only be an
        # aggregated dataframe for all countries, but also an additional one
        # for each country
        single_df_name <- paste0(area_name, "_lights")
        assign(single_df_name, all_aggregated, envir = .GlobalEnv)
      }
      
      if (i == 1){
        lights <- all_aggregated
        # build dataframe called lights with all areas, start with
        # first area, which means i = 1
      } else if (i > 1){
        lights <- rbind(lights, aggregated)
        # bind to the lights dataframe if area loop is
        # at least at the second iteration
      }
      
    } # end sequence loop
    
    rm(area_name)
    rm(ISO3)
    shapefile <- NULL
    
  } # end area loop
  
  lights <<- lights # load aggregated dataframe with all regions
  # into global environment
  
  if (lightdata_time == "yearly" &
      harmonized_lights == FALSE){
    if (dmsp_consistent == TRUE | length(sequence) == 1){
      print(paste0("The consistent DMSP version selected for your timespan is ",
                   dmsp_stump, "."))
    } else if (dmsp_consistent == FALSE & length(sequence) > 1){
      print(paste0("It was not possible to select a consistent DMSP version ",
                   "for your timespan. For each year, the newest DMSP version was chosen."))
    }
  }
  
} # end function
JakobMie/nightlightstats documentation built on April 11, 2025, 11:04 p.m.