#' Plot HYPE model subbasin routing.
#'
#' Plot routing of subbasins for a HYPE model on an interactive map.
#'
#' @param map Path to file containing subbasin polygon GIS data (e.g. shapefile or geopackage) or a \code{SpatialPolygonsDataFrame}
#' or \code{sf} object. For large maps, a small/simplified polygon file should be used as larger files can take an excessive amount of time to render.
#' @param map.subid.column Integer, column index in the \code{map} 'data' \code{\link{slot}} holding SUBIDs (sub-catchment IDs).
#' Only required if providing GeoData information with \code{gd}.
#' @param gd Path to model GeoData.txt or a GeoData object from \code{\link{ReadGeoData}}. Only required if \code{map} does not contain SUBID and/or MAINDOWN fields.
#' @param bd Path to model BranchData.txt or a BranchData object from \code{\link{ReadBranchData}}. Only required if model has a BranchData.txt file.
#' @param plot.scale Logical, include a scale bar on the map.
#' @param plot.searchbar Logical, if \code{TRUE}, then a search bar will be included. See [leaflet.extras::addSearchFeatures()].
#' @param weight Numeric, weight of subbasin boundary lines. See [leaflet::addPolygons()].
#' @param opacity Numeric, opacity of subbasin boundary lines. See [leaflet::addPolygons()].
#' @param fillColor String, color of subbasin polygons. See [leaflet::addPolygons()].
#' @param fillOpacity Numeric, opacity of subbasin polygons. See [leaflet::addPolygons()].
#' @param line.weight Numeric, weight of routing lines. See [leaflet::addPolylines()].
#' @param line.opacity Numeric, opacity of routing lines. See [leaflet::addPolylines()].
#' @param seed Integer, seed number to to produce repeatable color palette.
#' @param darken Numeric specifying the amount of darkening applied to the random color palette. Negative values will lighten the palette. See \code{\link{distinctColorPalette}}.
#' @param font.size Numeric, font size (px) for map subbasin labels.
#' @param file Save map to an image file by specifying the path to the desired output file using this argument. File extension must be specified.
#' See [mapview::mapshot()].
#' You may need to run [webshot::install_phantomjs()] the first time you save a map to an image file.
#' @param vwidth Numeric, width of the exported map image in pixels. See [webshot::webshot()].
#' @param vheight Numeric, height of the exported map image in pixels. See [webshot::webshot()].
#' @param html.name Save map to an interactive HTML file by specifying the path to the desired output file using this argument. File extension must be specified.
#' See [htmlwidgets::saveWidget()].
#'
#' @details
#' \code{PlotSubbasinRouting} generates an interactive Leaflet map with lines indicating the routing of flow between subbasins. GeoData information only needs
#' to be provided if the \code{map} GIS data does not include SUBID and/or MAINDOWN fields. BranchData information only needs to be provided if model has a
#' BranchData.txt file. Subbasin routing lines are randomly assigned a color using \code{\link{distinctColorPalette}}.
#'
#' @return
#' Returns an interactive Leaflet map.
#'
#' @examples
#' \dontrun{
#' PlotSubbasinRouting(
#' map = system.file("demo_model",
#' "gis", "Nytorp_map.gpkg",
#' package = "HYPEtools"
#' ),
#' gd = system.file("demo_model", "GeoData.txt", package = "HYPEtools"),
#' map.subid.column = 25
#' )
#' }
#'
#' @importFrom dplyr full_join %>% bind_rows filter across rename_with
#' @importFrom tidyselect matches
#' @importFrom rlang .data
#' @importFrom stats setNames
#' @export
PlotSubbasinRouting <- function(map, map.subid.column = 1, gd = NULL, bd = NULL, plot.scale = TRUE, plot.searchbar = FALSE,
weight = 0.5, opacity = 1, fillColor = "#4d4d4d", fillOpacity = 0.25, line.weight = 5, line.opacity = 1,
seed = NULL, darken = 0, font.size = 10, file = "", vwidth = 1424, vheight = 1000, html.name = "") {
# Check/Load Dependencies - do this here so that these packages are not required for the base HYPEtools installation
if (!all(
requireNamespace("sf", quietly = TRUE),
requireNamespace("leaflet", quietly = TRUE),
requireNamespace("leaflet.extras", quietly = TRUE),
requireNamespace("mapview", quietly = TRUE),
requireNamespace("htmlwidgets", quietly = TRUE)
)) {
# Warn that a dependency is not installed
stop('To use this function, please ensure that the following packages are installed: c("sf", "leaflet", "leaflet.extras", "mapview", "htmlwidgets")', call.=FALSE)
# Perform function
} else {
# Import GIS Data
if ("character" %in% class(map)) {
map <- sf::st_read(map)
} else if ("SpatialPolygonsDataFrame" %in% class(map)) {
map <- sf::st_as_sf(map)
}
# Reproject if not a lat/long CRS
if (sf::st_is_longlat(map) == FALSE) {
map <- map %>% sf::st_transform(sf::st_crs("+proj=longlat +datum=WGS84"))
}
# Rename columns to all uppercase except geometry column
map <- map %>%
rename_with(.fn = toupper, .cols = colnames(map)[which(!colnames(map) == attr(map, "sf_column"))])
# Get name of map subid column
map.subid.name <- colnames(map)[map.subid.column]
# Check if GeoData is required
if (is.null(gd) & !all(c("SUBID", "MAINDOWN") %in% colnames(map))) {
stop("SUBID and/or MAINDOWN fields are missing in GIS data, so a GeoData.txt file/object must be supplied.")
}
# Read GeoData if supplied a path
if (!is.null(gd) & typeof(gd) == "character") {
gd <- ReadGeoData(gd)
}
# Read BranchData if supplied a path
if (!is.null(bd) & typeof(bd) == "character") {
bd <- suppressMessages(ReadBranchData(bd))
}
# Format BranchData
if (!is.null(bd)) {
bd <- bd %>%
dplyr::filter(!is.na(.data$SOURCEID))
}
# Join GIS & GeoData if GeoData provided
if (!is.null(gd)) {
message(paste0('Joining "', colnames(map)[map.subid.column], '" from GIS Data (map) To "', "SUBID", '" from GeoData (gd)'))
map <- full_join(map[, map.subid.column] %>% mutate(across(1, ~ as.character(.x))), gd %>% mutate(across("SUBID", ~ as.character(.x))), by = setNames(nm = colnames(map)[map.subid.column], "SUBID")) # Join GIS Data with GeoData in a manner in which column names don't have to be identical (e.g. "SUBID" and "subid" is okay, character and integer is okay)
}
# Create Subbasin Points and remove rows where downstream subbasins don't exist
map_point <- suppressWarnings(sf::st_point_on_surface(map))
# Get Downstream Subbasin Point
message("Finding Downstream Subbasins")
map_point$ds_geometry <- sf::st_sfc(unlist(lapply(1:nrow(map_point), function(X) {
# Get Downstream SUBID
ds <- map_point$MAINDOWN[X]
# If Downstream SUBID Exists
if (ds %in% unlist(map_point[, map.subid.name] %>% sf::st_drop_geometry())) {
sf::st_geometry(map_point[which(unlist(map_point[, map.subid.name] %>% sf::st_drop_geometry()) == ds), attr(map_point, "sf_column")])
} else {
sf::st_sfc(sf::st_point(c(0, 0))) # Assign Point 0,0 and remove later
}
}), recursive = FALSE))
# Get Downstream Subbasin Points for Branches
if (!is.null(bd)) {
message("Finding Branch Subbasins")
for (i in 1:nrow(bd)) {
# Get row of data for source subbasin and change MAINDOWN to Branch subbasin
branch <- map_point[which(map_point[, map.subid.name] == bd$SOURCEID[i]), ] %>%
mutate(MAINDOWN = bd$BRANCHID[i])
# Get branch Geometry if branch SUBID Exists
if (nrow(branch) > 0) {
if (bd$BRANCHID[i] %in% map_point[, map.subid.name]) {
branch$ds_geometry <- sf::st_sfc(sf::st_geometry(map_point[which(map_point[, map.subid.name] == bd$BRANCHID[i]), attr(map, "sf_column")]))
} else {
branch$ds_geometry <- sf::st_sfc(sf::st_point(c(0, 0))) # Assign Point 0,0 and remove later
}
# Add row to map_point
map_point <- map_point %>%
bind_rows(branch)
}
}
}
# Remove Subbasins where downstream subbasin doesn't exist
map_point <- map_point %>%
dplyr::filter(.data$MAINDOWN %in% unlist(map_point[, map.subid.name] %>% sf::st_drop_geometry()))
# Create Leaflet Plot
message("Creating Map")
leafmap <- leaflet::leaflet(options = leaflet::leafletOptions(preferCanvas = TRUE)) %>%
leaflet::addTiles() %>%
leaflet::addLayersControl(
baseGroups = c("Map", "Street", "Topo", "Satellite"),
overlayGroups = c("Routing", "Subbasins"),
options = leaflet::layersControlOptions(collapsed = FALSE, autoIndex = TRUE)
) %>%
leaflet.extras::addResetMapButton() %>%
leaflet::addPolygons(
group = "Subbasins",
data = map,
color = "black",
weight = weight,
opacity = opacity,
fillColor = fillColor,
fillOpacity = fillOpacity,
label = paste(map[, map.subid.name]), # Add label so searchbar will work
labelOptions = leaflet::labelOptions(noHide = TRUE, textOnly = TRUE, style = list("color" = fillColor, "font-size" = "0px")) # Set label color and size to 0 to hide labels
) %>%
leaflet::addLabelOnlyMarkers(
group = "Subbasins",
data = suppressWarnings(sf::st_point_on_surface(map)),
label = map[[map.subid.name]],
labelOptions = leaflet::labelOptions(noHide = TRUE, direction = "auto", textOnly = TRUE, style = list("font-size" = paste0(font.size, "px")))
)
# Create function to get colors for polylines
color_pal <- function(X) {
tryCatch(distinctColorPalette(X, seed = seed, darken = darken), # Try to get a distinct color for each line
error = function(e) {
rep_len(distinctColorPalette(100, seed = seed, darken = darken), X) # If there is an error, then repeat palette of 100 colors as necessary
}
)
}
# Get colors for polylines
colors <- color_pal(nrow(map_point))
# Add Lines
message("Adding Routing Lines")
progress <- 1
for (i in 1:nrow(map_point)) {
# Add Progress Message for datasets with >=1000 polylines
if (nrow(map_point) >= 1000 & i == ceiling(nrow(map_point) / 10) * progress) {
message(paste0("Adding Routing Lines: ", progress * 10, "%"))
progress <- progress + 1
}
leafmap <- leafmap %>%
leaflet::addPolylines(
group = "Routing",
lat = c(sf::st_coordinates(map_point[attr(map, "sf_column")])[i, 2], sf::st_coordinates(map_point$ds_geometry)[i, 2]),
lng = c(sf::st_coordinates(map_point[attr(map, "sf_column")])[i, 1], sf::st_coordinates(map_point$ds_geometry)[i, 1]),
label = paste("SUBID", unlist(map_point[, map.subid.name] %>% sf::st_drop_geometry())[i], "to SUBID", map_point$MAINDOWN[i]),
color = colors[i],
weight = line.weight,
opacity = line.opacity
)
}
# Add searchbar to map
if (plot.searchbar == TRUE) {
leafmap <- leafmap %>%
leaflet.extras::addSearchFeatures(
targetGroups = "Subbasins",
options = leaflet.extras::searchFeaturesOptions(zoom = 10, hideMarkerOnCollapse = TRUE)
)
}
# Add scalebar to map
if (plot.scale == TRUE) {
leafmap <- leafmap %>%
leaflet::addScaleBar(position = "bottomright")
}
# Add various basemaps
leafmap <- leafmap %>%
leaflet::addProviderTiles("CartoDB.Positron", group = "Map") %>%
leaflet::addTiles(group = "Street") %>%
leaflet::addProviderTiles("Esri.WorldTopoMap", group = "Topo") %>%
leaflet::addProviderTiles("Esri.WorldImagery", group = "Satellite") %>%
leaflet::addProviderTiles("CartoDB.PositronOnlyLabels", group = "Satellite")
# Save Image
if (!file == "") {
message("Saving Image")
mapview::mapshot(leafmap, file = file, vwidth = vwidth, vheight = vheight, remove_controls = c("zoomControl", "layersControl", "homeButton", "drawToolbar", "easyButton"), selfcontained = FALSE)
}
# Save HTML
if (!html.name == "") {
message("Saving HTML")
temp <- file.path(tempdir(), basename(html.name))
htmlwidgets::saveWidget(leafmap, file = temp, title = sub(pattern = "(.*)\\..*$", replacement = "\\1", basename(html.name)), selfcontained = TRUE) # Save HTML file to temp directory so selfcontained=T works
file.rename(temp, html.name) # Rename/Move HTML file to desired file
}
return(leafmap)
}
}
# DEBUG:
# library(dplyr)
# library(sf)
# library(leaflet)
# library(leaflet.extras)
# map.subid.column <- 1
# gd <- NULL
# bd <- NULL
# plot.scale <- T
# plot.searchbar <- F
# weight <- 0.15
# opacity <- 0.75
# fillColor <- "#4d4d4d"
# fillOpacity <- 0.25
# line.weight <- 5
# line.opacity <- 1
# font.size <- 10
# file <- ""
# vwidth <- 1424
# vheight <- 1000
# html.name <- ""
# selfcontained <- FALSE
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.