Introduction to the assignment
This assignment consists of three main sections.
In the first section, you need to select one Census Tract that you think is the most walkable and another one that you think is least walkable within Fulton and DeKalb Counties, GA. As long as they are within the two counties, you can pick any two Census Tracts you want. If the area you want to use as walkable/unwalkable area is not well-covered by a single Census Tract, you can select multiple tracts (e.g., selecting three adjacent tracts as one walkable area). The definition of ‘walkable’ can be your own - you can choose solely based on your experience (e.g., had best/worst walking experience because …), refer to Walk Score, or any other mix of criteria you want. After you make the selection, provide a short write-up of why you chose those Census Tracts.
The second section is the main part of this assignment in which you prepare OSM data, download GSV images, apply computer vision technique we learned in the class (i.e., semantic segmentation).
In the third section, you will summarise and analyze the output and provide your findings. After you apply computer vision to the images, you will have the number of pixels in each image that represent 150 categories in your data. You will focus on the following categories in your analysis: building, sky, tree, road, and sidewalk. Specifically, you will (1) create maps to visualize the spatial distribution of different objects, (2) compare the mean of each category between the two Census Tract and (3) draw boxplots to compare the distributions.
Section 1. Choose your Census Tracts.
Provide a brief description of your census tracts. Why do you think the Census Tracts are walkable and unwalkable? What were the contributing factors?
Walkable: Downtown Decatur (0225.01). This tract encompasses the heart of Downtown Decatur, known for its pedestrian-friendly environment.
Unwalkable: Cmap Creek Marketplace (0113.06). This tract includes the Camp Creek Marketplace area, characterized by large retail centers.
Section 2. OSM, GSV, and computer vision.
Fill out the template to complete the script.
library(tidyverse)
library(tidycensus)
library(osmdata)
library(sfnetworks)
library(units)
library(sf)
library(tidygraph)
library(tmap)
library(here)
Step 1. Get OSM data and clean it.
The getbb() function, which we used in the class to
download OSM data, isn’t suitable for downloading just two Census
Tracts. We will instead use an alternative method.
- Using tidycensus package, download the Census Tract polygon for Fulton and DeKalb counties.
- Extract two Census Tracts, each of which will be your most walkable and least walkable Census Tracts.
- Using their bounding boxes, get OSM data.
- Convert them into sfnetwork object and clean it.
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Set up your api key here
census_api_key(Sys.getenv("census_api"))
## To install your API key for use in future sessions, run this function with `install = TRUE`.
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download Census Tract polygon for Fulton and DeKalb
tract <- get_acs("tract",
variables = c('tot_pop' = 'B01001_001'),
year = 2022,
state = "GA",
county = c("Fulton", "DeKalb"),
geometry = TRUE)
## Getting data from the 2018-2022 5-year ACS
## Downloading feature geometry from the Census website. To cache shapefiles for use in future sessions, set `options(tigris_use_cache = TRUE)`.
## | | | 0% | | | 1% | |== | 2% | |== | 3% | |== | 4% | |=== | 5% | |==== | 6% | |===== | 7% | |====== | 8% | |====== | 9% | |======= | 9% | |======== | 12% | |========= | 13% | |========== | 15% | |============ | 16% | |============= | 18% | |============== | 21% | |================ | 22% | |================= | 24% | |==================== | 28% | |==================== | 29% | |====================== | 31% | |======================== | 34% | |========================= | 36% | |========================== | 37% | |=========================== | 38% | |=========================== | 39% | |============================= | 41% | |============================== | 43% | |================================= | 47% | |================================== | 49% | |==================================== | 51% | |===================================== | 52% | |====================================== | 54% | |======================================== | 57% | |========================================= | 59% | |=========================================== | 61% | |============================================ | 63% | |============================================= | 64% | |=================================================== | 72% | |=================================================== | 73% | |===================================================== | 75% | |============================================================ | 86% | |============================================================= | 88% | |================================================================== | 94% | |==================================================================== | 96% | |==================================================================== | 97% | |======================================================================| 100%
# =========== NO MODIFY ZONE ENDS HERE ========================================
# TASK ////////////////////////////////////////////////////////////////////////
# The purpose of this TASK is to create one bounding box for walkable Census Tract and another bounding box for unwalkable Census Tract.
# As long as you generate what's needed for the subsequent codes, you are good. The numbered list of tasks below is to provide some hints.
# 1. Write the GEOID of walkable & unwalkable Census Tracts. e.g., tr1_ID <- c("13121001205", "13121001206")
# 2. Extract the selected Census Tracts using tr1_ID & tr2_ID
# 3. Create their bounding boxes using st_bbox(), and
# 4. assign them to tract_1_bb and tract_1_bb, respectively.
# For the walkable Census Tract(s)
# 1.
tr1_ID <- c("13089022501")
# 2~4
tract_1_bb <- tract %>%
filter(GEOID %in% tr1_ID) %>%
st_bbox()
# For the unwalkable Census Tract(s)
# 1.
tr2_ID <- c("13121011306")
# 2~4
tract_2_bb <- tract %>%
filter(GEOID %in% tr2_ID) %>%
st_bbox()
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Get OSM data for the two bounding box
osm_1 <- opq(bbox = tract_1_bb) %>%
add_osm_feature(key = 'highway',
value = c("motorway", "trunk", "primary",
"secondary", "tertiary", "unclassified",
"residential")) %>%
osmdata_sf() %>%
osm_poly2line()
osm_2 <- opq(bbox = tract_2_bb) %>%
add_osm_feature(key = 'highway',
value = c("motorway", "trunk", "primary",
"secondary", "tertiary", "unclassified",
"residential")) %>%
osmdata_sf() %>%
osm_poly2line()
# =========== NO MODIFY ZONE ENDS HERE ========================================
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Convert osm_1 and osm_2 to sfnetworks objects (set directed = FALSE)
# 2. Clean the network by (1) deleting parallel lines and loops, (2) create missing nodes, and (3) remove pseudo nodes,
# 3. Add a new column named length using edge_length() function.
net1 <- osm_1$osm_lines %>%
# Drop redundant columns
select(osm_id, highway) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
activate("edges") %>%
filter(!edge_is_multiple()) %>%
filter(!edge_is_loop()) %>%
convert(., sfnetworks::to_spatial_subdivision) %>%
convert(., sfnetworks::to_spatial_smooth)
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
net1 <- net1 %>%
st_transform(4326) %>%
activate("edges") %>%
mutate(length = edge_length())
net2 <- osm_2$osm_lines %>%
# Drop redundant columns
select(osm_id, highway) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
activate("edges") %>%
filter(!edge_is_multiple()) %>%
filter(!edge_is_loop()) %>%
convert(., sfnetworks::to_spatial_subdivision) %>%
convert(., sfnetworks::to_spatial_smooth)
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
net2 <- net2 %>%
st_transform(4326) %>%
activate("edges") %>%
mutate(length = edge_length())
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# OSM for the walkable part
edges_1 <- net1 %>%
# Extract 'edges'
st_as_sf("edges") %>%
# Drop segments that are too short (100m)
mutate(length = as.vector(length)) %>%
filter(length > 100) %>%
# Add a unique ID for each edge
mutate(edge_id = seq(1,nrow(.)),
is_walkable = "walkable")
# OSM for the unwalkable part
edges_2 <- net2 %>%
# Extract 'edges'
st_as_sf("edges") %>%
# Drop segments that are too short (100m)
mutate(length = as.vector(length)) %>%
filter(length > 100) %>%
# Add a unique ID for each edge
mutate(edge_id = seq(1,nrow(.)),
is_walkable = "unwalkable")
# Merge the two
edges <- bind_rows(edges_1, edges_2)
# =========== NO MODIFY ZONE ENDS HERE ========================================
Step 2. Define getAzimuth() function.
getAzimuth <- function(line){
# This function takes one edge (i.e., a street segment) as an input and
# outputs a data frame with four points (start, mid1, mid2, and end) and their azimuth.
# TASK ////////////////////////////////////////////////////////////////////////
# 1. From `line` object, extract the coordinates using st_coordinates() and extract the first two rows.
# 2. Use atan2() function to calculate the azimuth in degree.
# Make sure to adjust the value such that 0 is north, 90 is east, 180 is south, and 270 is west.
# 1
start_p <- line %>%
st_coordinates() %>%
.[1:2,1:2]
# 2
start_azi <- atan2(start_p[2,"X"] - start_p[1, "X"],
start_p[2,"Y"] - start_p[1, "Y"])*180/pi
# //TASK //////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# Repeat what you did above, but for last two rows (instead of the first two rows).
# Remember to flip the azimuth so that the camera would be looking at the street that's being measured
end_p <- line %>%
st_coordinates() %>%
.[(nrow(.)-1):nrow(.),1:2]
end_azi <- atan2(end_p[2,"X"] - end_p[1, "X"],
end_p[2,"Y"] - end_p[1, "Y"])*180/pi
end_azi <- if (end_azi < 180) {end_azi + 180} else {end_azi - 180}
# //TASK //////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# 1. From `line` object, use st_line_sample() function to generate points at 0.45 and 0.55 locations. These two points will be used to calculate the azimuth.
# 2. Use st_case() function to convert 'MULTIPOINT' object to 'POINT' object.
# 3. Extract coordinates using st_coordinates().
# 4. Use atan2() functino to Calculate azimuth.
# 5. Use st_line_sample() again to generate a point at 0.5 location and get its coordinates. This point will be the location at which GSV image will be downloaded.
mid_p3 <- line %>%
st_line_sample(sample = c(0.45, 0.5, 0.55)) %>%
st_cast("POINT") %>%
st_coordinates()
mid_p <- mid_p3[2,]
mid_azi <- atan2(mid_p3[2,"X"] - mid_p3[1, "X"],
mid_p3[2,"Y"] - mid_p3[1, "Y"])*180/pi
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
return(tribble(
~type, ~X, ~Y, ~azi,
"start", start_p[1,"X"], start_p[1,"Y"], start_azi,
"mid1", mid_p["X"], mid_p["Y"], mid_azi,
"mid2", mid_p["X"], mid_p["Y"], ifelse(mid_azi < 180, mid_azi + 180, mid_azi - 180),
"end", end_p[2,"X"], end_p[2,"Y"], end_azi))
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
Step 3. Apply the function to all street segments
We can apply getAzimuth() function to the edges object.
We finally append edges object to make use of the columns
in edges object (e.g., is_walkable column).
When you are finished with this code chunk, you will be ready to
download GSV images.
# TASK ////////////////////////////////////////////////////////////////////////
# Apply getAzimuth() function to all edges.
# Remember that you need to pass edges object to st_geometry() before you apply getAzimuth()
edges_azi <- edges %>%
st_geometry() %>%
map_df(getAzimuth, .progress = T)
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
edges_azi <- edges_azi %>%
bind_cols(edges %>%
st_drop_geometry() %>%
slice(rep(1:nrow(edges),each=4))) %>%
st_as_sf(coords = c("X", "Y"), crs = 4326, remove=FALSE) %>%
mutate(node_id = seq(1, nrow(.)))
# =========== NO MODIFY ZONE ENDS HERE ========================================
Step 4. Define a function that formats request URL and download images.
getImage <- function(iterrow){
# This function takes one row of edges_azi and downloads GSV image using the information from edges_azi.
# TASK ////////////////////////////////////////////////////////////////////////
# Finish this function definition.
# 1. Extract required information from the row of edges_azi, including
# type (i.e., start, mid1, mid2, end), location, heading, edge_id, node_id, and key.
# 2. Format the full URL and store it in `request`. Refer to this page: https://developers.google.com/maps/documentation/streetview/request-streetview
# 3. Format the full path (including the file name) of the image being downloaded and store it in `fpath`
type <- iterrow$type
location <- paste0(iterrow$Y %>% round(5), ",", iterrow$X %>% round(5))
heading <- iterrow$azi %>% round(1)
edge_id <- iterrow$edge_id
node_id <- iterrow$node_id
key <- Sys.getenv("google_api")
endpoint <- "https://maps.googleapis.com/maps/api/streetview"
request <- glue::glue("{endpoint}?size=640x640&location={location}&heading={heading}&fov=90&pitch=0&key={key}")
fname <- glue::glue("GSV-nid_{node_id}-eid_{edge_id}-type_{type}-Location_{location}-heading_{heading}.jpg") # Don't change this code for fname
fpath <- here("images", fname)
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download images
if (!file.exists(fpath)){
download.file(request, fpath, mode = 'wb')
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
Step 5. Download GSV images
Before you download GSV images, make sure the row
number of edges_azi is not too large! The row number of
edges_azi will be the number of GSV images you will be
downloading. Before you download images, always double-check your Google
Cloud Console’s Billing tab to make sure that you will not go above the
free credit of $200 each month. The price is $7 per 1000 images.
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Loop!
for (i in seq(1,nrow(edges_azi))){
getImage(edges_azi[i,])
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
ZIP THE DOWNLOADED IMAGES AND NAME IT ‘gsv_images.zip’ FOR STEP 6.
Step 6. Apply computer vision
Now, use Google Colab to apply the semantic segmentation model. Zip your images and upload the images to your Colab session.
Step 7. Merging the processed data back to R
Once all of the images are processed and saved in your Colab session as a CSV file, download the CSV file and merge it back to edges.
# TASK ////////////////////////////////////////////////////////////////////////
# Read the downloaded CSV file from Google Colab
seg_output <- read.csv("seg_output.csv")
# //TASK ////////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Join the seg_output object back to edges_azi object using node_id as the join key.
edges_seg_output <- edges_azi %>%
inner_join(seg_output, by=c("node_id" = "img_id")) %>%
select(type, X, Y, node_id, building, sky, tree, road, sidewalk, is_walkable) %>%
mutate(across(c(building, sky, tree, road, sidewalk), function(x) x/(640*640)))
# =========== NO MODIFY ZONE ENDS HERE ========================================
Section 3. Summarise and analyze the results.
At the beginning of this assignment, you defined one Census Tract as walkable and the other as unwalkable. The key to the following analysis is the comparison between walkable and unwalkable Census Tracts.
Analysis 1 - Create interactive map(s) to visualize the spatial distribution of the streetscape.
You need to create maps of the proportion of building, sky, tree, road, and sidewalk for walkable and unwalkable areas. In total, you will have 10 maps.
I used the color palette “-Spectral” to enhance visualization and included two maps of the same factor for the two Census Tracts in a single interactive map to facilitate comparison.
# TASK ////////////////////////////////////////////////////////////////////////
# Create interactive map(s) to visualize the `edges_seg_output` objects.
# As long as you can deliver the message clearly, you can use any format/package you want.
walkable_data <- edges_seg_output %>% filter(is_walkable == "walkable")
unwalkable_data <- edges_seg_output %>% filter(is_walkable == "unwalkable")
#library(tmaptools)
#palette_explorer() # Opens an interactive viewer for palettes
# //TASK //////////////////////////////////////////////////////////////////////
Building
Both Census Tracts display low building proportions in the street view images, suggesting limited building visibility within the streetscape.
tmap_mode("view");
## tmap mode set to interactive viewing
building_walkable <- tm_shape(walkable_data) +
tm_dots("building", title = "Building Proportion", palette = "-Spectral") +
tm_layout(title = "Walkable: Building Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
building_unwalkable <- tm_shape(unwalkable_data) +
tm_dots("building", title = "Building Proportion", palette = "-Spectral") +
tm_layout(title = "Unwalkable: Building Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
tmap_arrange(building_walkable, building_unwalkable, sync = FALSE)
Sky
The walkable Census Tract predominantly exhibits low sky proportions, while the unwalkable Census Tract shows greater variance. The unwalkable map displays more red and yellow dots, indicating areas with higher sky proportions, suggesting a lack of shade on the streets.
tmap_mode("view");
## tmap mode set to interactive viewing
# Sky Proportion
sky_walkable <- tm_shape(walkable_data) +
tm_dots("sky", title = "Sky Proportion", palette = "-Spectral") +
tm_layout(title = "Walkable: Sky Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
sky_unwalkable <- tm_shape(unwalkable_data) +
tm_dots("sky", title = "Sky Proportion", palette = "-Spectral") +
tm_layout(title = "Unwalkable: Sky Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
tmap_arrange(sky_walkable, sky_unwalkable, sync = FALSE)
Tree
The walkable Census Tract predominantly exhibits medium to high tree proportions, while the unwalkable Census Tract shows greater variability, with clusters of both high and low tree proportions, reflecting uneven vegetation distribution. The tree proportion may also be inversely related to the sky proportion.
tmap_mode("view");
## tmap mode set to interactive viewing
# Tree Proportion
tree_walkable <- tm_shape(walkable_data) +
tm_dots("tree", title = "Tree Proportion", palette = "-Spectral") +
tm_layout(title = "Walkable: Tree Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
tree_unwalkable <- tm_shape(unwalkable_data) +
tm_dots("tree", title = "Tree Proportion", palette = "-Spectral") +
tm_layout(title = "Unwalkable: Tree Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
tmap_arrange(tree_walkable, tree_unwalkable, sync = FALSE)
Road
The walkable Census Tract exhibits medium to high road proportions, and little amount of low road proportion.with few areas of low road proportions. In contrast, the unwalkable Census Tract shows greater variability, featuring clusters of both high and low road proportions, has more low road proportion points.
tmap_mode("view")
## tmap mode set to interactive viewing
road_walkable <- tm_shape(walkable_data) +
tm_dots("road", title = "Road Proportion", palette = "-Spectral") +
tm_layout(title = "Walkable: Road Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
road_unwalkable <- tm_shape(unwalkable_data) +
tm_dots("road", title = "Road Proportion", palette = "-Spectral") +
tm_layout(title = "Unwalkable: Road Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
tmap_arrange(road_walkable, road_unwalkable, sync = FALSE)
Sidewalk
Both the walkable Census Tract and the unwalkable Census Tract have low proportion of sidewalk through whole data, but the walkable Census Tract has more yellow and red point than the unwalkable Census Tract.
tmap_mode("view")
## tmap mode set to interactive viewing
fixed_breaks <- seq(0, 0.35, by = 0.35/3)
# Sidewalk Proportion
sidewalk_walkable <- tm_shape(walkable_data) +
tm_dots("sidewalk", title = "Sidewalk Proportion", palette = "-Spectral", breaks = fixed_breaks) +
tm_layout(title = "Walkable: Sidewalk Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
sidewalk_unwalkable <- tm_shape(unwalkable_data) +
tm_dots("sidewalk", title = "Sidewalk Proportion", palette = "-Spectral", breaks = fixed_breaks) +
tm_layout(title = "Unwalkable: Sidewalk Proportion", legend.show = TRUE) +
tm_scale_bar(position = c("left", "bottom"))
tmap_arrange(sidewalk_walkable, sidewalk_unwalkable, sync = FALSE)
Analysis 2 - Compare the means.
You need to calculate the mean of the proportion of building, sky, tree, road, and sidewalk for walkable and unwalkable areas. For example, you need to calculate the mean of building category for each of walkable and unwalkable Census Tracts. Then, you need to calculate the mean of sky category for each of walkable and unwalkable Census Tracts. In total, you will have 10 mean values.
The walkable Census Tract shows higher mean proportions of buildings, trees, roads, and sidewalks compared to the unwalkable Census Tract. In contrast, the unwalkable Census Tract has a higher sky proportion, reflecting less overhead coverage from vegetation or structures. The higher tree and sidewalk proportions in the walkable area contribute to better shading and pedestrian accessibility.
# TASK ////////////////////////////////////////////////////////////////////////
# Perform the calculation as described above.
# As long as you can deliver the message clearly, you can use any format/package you want.
# Calculate means for walkable Census Tract
walkable_means <- walkable_data %>%
summarize(
mean_building = mean(building, na.rm = TRUE),
mean_sky = mean(sky, na.rm = TRUE),
mean_tree = mean(tree, na.rm = TRUE),
mean_road = mean(road, na.rm = TRUE),
mean_sidewalk = mean(sidewalk, na.rm = TRUE)
)
# Calculate means for unwalkable Census Tract
unwalkable_means <- unwalkable_data %>%
summarize(
mean_building = mean(building, na.rm = TRUE),
mean_sky = mean(sky, na.rm = TRUE),
mean_tree = mean(tree, na.rm = TRUE),
mean_road = mean(road, na.rm = TRUE),
mean_sidewalk = mean(sidewalk, na.rm = TRUE)
)
# Combine results for comparison
mean_comparison <- bind_rows(
walkable_means %>% mutate(type = "walkable"),
unwalkable_means %>% mutate(type = "unwalkable")
)
# Display results
print(mean_comparison)
## Simple feature collection with 2 features and 6 fields
## Geometry type: MULTIPOINT
## Dimension: XY
## Bounding box: xmin: -84.53242 ymin: 33.61533 xmax: -84.29274 ymax: 33.78096
## Geodetic CRS: WGS 84
## # A tibble: 2 × 7
## mean_building mean_sky mean_tree mean_road mean_sidewalk
## * <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0.0475 0.221 0.255 0.330 0.0259
## 2 0.0173 0.399 0.191 0.283 0.00860
## # ℹ 2 more variables: geometry <MULTIPOINT [°]>, type <chr>
# //TASK //////////////////////////////////////////////////////////////////////
Analysis 3 - Draw boxplot
1. Walkable tracts have higher building proportions overall, with more variability compared to unwalkable tracts. 2. Unwalkable tracts exhibit higher sky proportions than walkable tracts. 3. Walkable tracts show higher tree proportions than Unwalkable tracts. 4. Road proportions are similar mean between the two types, but walkable tracts show more consistency and higher values 5. Walkable tracts have higher sidewalk proportions than Unwalkable tracts.
# TASK ////////////////////////////////////////////////////////////////////////
# Create boxplot(s) using ggplot2 package.
data_long <- edges_seg_output %>%
select(is_walkable, building, sky, tree, road, sidewalk) %>%
pivot_longer(
cols = c(building, sky, tree, road, sidewalk),
names_to = "variable",
values_to = "proportion"
)
ggplot(data_long, aes(x = is_walkable, y = proportion, fill = is_walkable)) +
geom_boxplot() +
facet_wrap(~ variable, scales = "free_y") +
labs(
title = "Distribution of Proportions",
x = "Walkability",
y = "Proportion"
) +
scale_fill_manual(values = c("walkable" = "green", "unwalkable" = "red")) +
theme_minimal()
# //TASK //////////////////////////////////////////////////////////////////////