#30DMC_template

Author

Daniele Cannatella

Published

October 25, 2024

#30daysmapchallenge template

This template is designed to guide you through the challenge, providing structured steps and helpful resources for each mapping task. Whether you’re a seasoned cartographer or just starting your journey, we hope you find inspiration and joy in visualizing data and sharing your work with the community. Let’s embark on this mapping adventure together!

This template uses a polygon map as an example to illustrate the process of creating visually engaging and informative maps with R. It is designed to guide you through the challenge, providing structured steps and helpful resources for each mapping task.

A polygon Map

1. Package Installation and Loading


Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
Linking to GEOS 3.9.3, GDAL 3.5.2, PROJ 8.2.1; sf_use_s2() is TRUE
Loading required package: sysfonts
Loading required package: showtextdb
here() starts at C:/Users/dcannatella/surfdrive/01_Research/2024-30daysmapchallenge/30DayMapChallenge2024
[[1]]
[1] "ggplot2"   "stats"     "graphics"  "grDevices" "utils"     "datasets" 
[7] "methods"   "base"     

[[2]]
[1] "dplyr"     "ggplot2"   "stats"     "graphics"  "grDevices" "utils"    
[7] "datasets"  "methods"   "base"     

[[3]]
 [1] "sf"        "dplyr"     "ggplot2"   "stats"     "graphics"  "grDevices"
 [7] "utils"     "datasets"  "methods"   "base"     

[[4]]
 [1] "readr"     "sf"        "dplyr"     "ggplot2"   "stats"     "graphics" 
 [7] "grDevices" "utils"     "datasets"  "methods"   "base"     

[[5]]
 [1] "tidyr"     "readr"     "sf"        "dplyr"     "ggplot2"   "stats"    
 [7] "graphics"  "grDevices" "utils"     "datasets"  "methods"   "base"     

[[6]]
 [1] "rmapshaper" "tidyr"      "readr"      "sf"         "dplyr"     
 [6] "ggplot2"    "stats"      "graphics"   "grDevices"  "utils"     
[11] "datasets"   "methods"    "base"      

[[7]]
 [1] "showtext"   "showtextdb" "sysfonts"   "rmapshaper" "tidyr"     
 [6] "readr"      "sf"         "dplyr"      "ggplot2"    "stats"     
[11] "graphics"   "grDevices"  "utils"      "datasets"   "methods"   
[16] "base"      

[[8]]
 [1] "here"       "showtext"   "showtextdb" "sysfonts"   "rmapshaper"
 [6] "tidyr"      "readr"      "sf"         "dplyr"      "ggplot2"   
[11] "stats"      "graphics"   "grDevices"  "utils"      "datasets"  
[16] "methods"    "base"      
All specified packages have been loaded successfully!

2. Import 3DBAG tiles

tiles <- st_read(here("template/data/3DBAG_tiles.shp"))
Reading layer `3DBAG_tiles' from data source 
  `C:\Users\dcannatella\surfdrive\01_Research\2024-30daysmapchallenge\30DayMapChallenge2024\template\data\3DBAG_tiles.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 15086 features and 2 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 13593.34 ymin: 306890.4 xmax: 333593.3 ymax: 626890.4
Projected CRS: Amersfoort / RD New

3. Calculate building density per tile

# Remove duplicate geometries from tiles

tiles <- tiles %>%   st_as_sf() %>%  # Ensure it's an sf object
  distinct()  # Remove duplicate rows based on geometry and attributes

tiles$area <- as.numeric(st_area(tiles))

tiles$bdens <- tiles$obj_nr_bui / (tiles$area/1000000)
# Calculate density (n. buildings/km2)


tiles
Simple feature collection with 7543 features and 4 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 13593.34 ymin: 306890.4 xmax: 333593.3 ymax: 626890.4
Projected CRS: Amersfoort / RD New
First 10 features:
      tile_id obj_nr_bui                       geometry      area        bdens
1     3/256/0         30 POLYGON ((77593.34 306890.4... 4.096e+09  0.007324219
2  3/1024/512          6 POLYGON ((269593.3 434890.4... 4.096e+09  0.001464844
3     4/512/0       3958 POLYGON ((141593.3 306890.4... 1.024e+09  3.865234375
4   5/128/512        255 POLYGON ((45593.34 434890.4... 2.560e+08  0.996093750
5   5/768/128        185 POLYGON ((205593.3 338890.4... 2.560e+08  0.722656250
6   5/768/192       4239 POLYGON ((205593.3 354890.4... 2.560e+08 16.558593750
7   5/512/768       2076 POLYGON ((141593.3 498890.4... 2.560e+08  8.109375000
8   5/512/896       2132 POLYGON ((141593.3 530890.4... 2.560e+08  8.328125000
9   5/512/960       1842 POLYGON ((141593.3 546890.4... 2.560e+08  7.195312500
10  5/960/512         41 POLYGON ((253593.3 434890.4... 2.560e+08  0.160156250
plot(tiles)

4. Import city labels from Natural Earth Data

cities_labels <- st_read(here("template/data/ne_10m_populated_places_simple.shp"))
Reading layer `ne_10m_populated_places_simple' from data source 
  `C:\Users\dcannatella\surfdrive\01_Research\2024-30daysmapchallenge\30DayMapChallenge2024\template\data\ne_10m_populated_places_simple.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 7342 features and 31 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -179.59 ymin: -90 xmax: 179.3833 ymax: 82.48332
Geodetic CRS:  WGS 84
# Transform the CRS of cities_labels to match tiles extent

cities_labels <- st_transform(cities_labels, crs = st_crs(tiles))

# Perform the intersection

cities_labels <- st_intersection(tiles, cities_labels)
Warning: attribute variables are assumed to be spatially constant throughout
all geometries
cities_labels <- cities_labels %>%   filter(iso_a2 == "NL")  # Check the result plot(st_geometry(cities_labels))

5. Import provinces from CBS

provincies <- st_read(here("template/data/provincies.shp"))
Reading layer `provincies' from data source 
  `C:\Users\dcannatella\surfdrive\01_Research\2024-30daysmapchallenge\30DayMapChallenge2024\template\data\provincies.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 12 features and 6 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 10425.16 ymin: 306846.2 xmax: 278026.1 ymax: 621876.3
Projected CRS: Amersfoort / RD New

6. Simplify provinces

# Step 1: Simplify the polygons (reduce number of vertices)
provincies_simple <- ms_simplify(provincies, keep = 0.01)  # Keep 1% of original vertices

# Step 2: Extract the simplified polygon coordinates
# Use st_geometry() to extract only the geometries, and then apply st_coordinates
simple_coords <- st_coordinates(st_geometry(provincies_simple))

# Step 3: Snap the coordinates to 8 cardinal directions
# Define the cardinal directions (N, S, E, W, NE, NW, SE, SW)
directions <- c(0, 45, 90, 135, 180, 225, 270, 315)  # Angles in degrees

# Define the snapping function
snap_to_direction <- function(x, y, directions) {
  # Calculate angle from origin (x, y) and snap to the nearest direction
  angle <- atan2(y, x) * 180 / pi
  angle <- (angle + 360) %% 360  # Normalize angle to [0, 360]
  closest_direction <- directions[which.min(abs(directions - angle))]
  
  # Calculate the new snapped coordinates
  rad <- closest_direction * pi / 180
  new_x <- cos(rad) * sqrt(x^2 + y^2)
  new_y <- sin(rad) * sqrt(x^2 + y^2)
  
  return(c(new_x, new_y))
}

# Apply the snapping function to each vertex (loop over the coordinates)
snapped_coords <- apply(simple_coords, 1, function(coord) snap_to_direction(coord[1], coord[2], directions))

# Reshape snapped_coords into the correct format for polygons (a matrix of x, y)
snapped_matrix <- matrix(snapped_coords, ncol = 2, byrow = TRUE)

# Step 4: Close the polygon by ensuring the first and last vert
provincies_simple2 <- st_simplify(provincies_simple, dTolerance = 2000)  # More simplification
# even more simplification!

# Load or create your spatial data
# For example, using a sample of tiles
# tiles <- st_read("path_to_your_shapefile")

# Step 1: Simplify the polygons (reduce number of vertices)
provincies_simple3 <- ms_simplify(provincies_simple2, keep = 0.5)  # Keep 1% of original vertices

# Step 2: Extract the simplified polygon coordinates
# Use st_geometry() to extract only the geometries, and then apply st_coordinates
simple_coords <- st_coordinates(st_geometry(provincies_simple3))

# Step 3: Snap the coordinates to 8 cardinal directions
# Define the cardinal directions (N, S, E, W, NE, NW, SE, SW)
directions <- c(0, 45, 90, 135, 180, 225, 270, 315)  # Angles in degrees

# Define the snapping function
snap_to_direction <- function(x, y, directions) {
  # Calculate angle from origin (x, y) and snap to the nearest direction
  angle <- atan2(y, x) * 180 / pi
  angle <- (angle + 360) %% 360  # Normalize angle to [0, 360]
  closest_direction <- directions[which.min(abs(directions - angle))]
  
  # Calculate the new snapped coordinates
  rad <- closest_direction * pi / 180
  new_x <- cos(rad) * sqrt(x^2 + y^2)
  new_y <- sin(rad) * sqrt(x^2 + y^2)
  
  return(c(new_x, new_y))
}

# Apply the snapping function to each vertex (loop over the coordinates)
snapped_coords <- apply(simple_coords, 1, function(coord) snap_to_direction(coord[1], coord[2], directions))

# Reshape snapped_coords into the correct format for polygons (a matrix of x, y)
snapped_matrix <- matrix(snapped_coords, ncol = 2, byrow = TRUE)

# Step 4: Close the polygon by ensuring the first and last vert
provincies_simple4 <- st_make_valid(provincies_simple3)
provincies_simple4 <- st_snap(provincies_simple4, provincies_simple4, tolerance = 0.001)
# Apply a small buffer to close gaps or remove overlaps
buffered_polygons <- st_buffer(provincies_simple4, dist = 2000) # Small positive buffer
unbuffered_polygons <- st_buffer(buffered_polygons, dist = -2000) # Remove the buffer
provincies_simple5 <- buffered_polygons

Plot the Polygon map

1. Set the ggplot

tiles_p <- ggplot()+
  
  geom_sf(data=provincies_simple5, fill="#103251", color="#EDF292")+
  
  geom_sf(data=tiles, fill="NA", color="#103251", size = 0.1, linetype = "dotted", alpha=0.1)+
  
  geom_sf(data=tiles, aes(alpha=bdens), fill="white", color=NA)+
    scale_alpha_continuous(range = c(0.05,1)) +
  
  geom_sf(data = cities_labels, color = "#EDF292", size = 1, alpha = 0.25)+
  
  geom_sf_text(data = cities_labels, aes(label = name), color = "#EDF292",
               family="script",
               alpha=1,
               nudge_x = 0.1,  # Adjust this value to move the labels horizontally
               nudge_y = 0.1  # Adjust this value to move the labels vertically
               )
  
  # Add city labels with nudging to avoid overlap with the polygons
  #geom_sf_label(data = cities_labels, 
  #              aes(label = name), color = "#EDF292",
  #              nudge_x = 0.2,  # Adjust horizontally
  #              nudge_y = 0.2,  # Adjust vertically
  #              label.size = 0.1, 
  #              family = "script")

tiles_p
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

2. Style and export the map

2.1 Add custom fonts

# Add Google Fonts to the system
showtext_auto()  # Automatically use showtext for text rendering
font_add_google("Roboto", "roboto")  # Add the "Roboto" font from Google Fonts
font_add_google("Lobster", "script")  # Add the "Roboto" f

2.2 Plot your map

tiles_p <- tiles_p +
  theme_void()+
  theme(
    plot.background = element_rect(fill = "#103251", color = NA),  # Set bg color
    plot.margin = margin(10, 10, 10, 10),  # Adjust margins
    legend.position = "bottom",  # Move the legend to the bottom
    legend.box = "horizontal",  # Arrange legends horizontally in bottom
    legend.title = element_blank(),  # Remove the title for the fill legend
    legend.text = element_text(size = 13,
                              family = "script",
                              color = "white"),  # Adjust text size and fon
    plot.title = element_text(size = 34,
                              face = "bold",
                              family = "script",
                              color = "white"),  # Title font customization
    plot.subtitle = element_text(size = 13,
                              family = "script",
                              color = "white"),  # Adjust text size and font legend
    plot.caption = element_text(size = 13,
                              family = "script",
                              color = "white"),
    legend.key.height = unit(1, "cm"),  # Adjust height to make keys squared
    legend.key.width = unit(1, "cm"),   # Adjust width to match height
  )+
  
   # Add a title to the map
  ggtitle("How many buildings are in the Netherlands?") +  # Add your map title here

  # Control legend appearance
  labs(
    title = "Building density in the Netherlands",
    subtitle = "per 3D bag tile",
    caption = "Source: 3DBAG, Author: Daniele Cannatella",
    fill = "Object Number") +  # Add a label for the fill legend

  # Control legend appearance
  # Adjust the legend to have two rows
  guides(
    fill = guide_legend(ncol = 1, nrow = 2, keyheight = unit(0.25, "cm"), keywidth = unit(0.25, "cm"))
  )


tiles_p

# Define the output file name
output_file <- "template/output/example_map.png"

# Export the map as a PNG with 1:1 aspect ratio
ggsave(filename = output_file, plot = tiles_p, device = "png", 
       width = 6, height = 6, units = "in", dpi = 300)

# Print a message to confirm export
cat("Map has been exported as", output_file, "with a 1:1 aspect ratio.\n")
Map has been exported as template/output/example_map.png with a 1:1 aspect ratio.
# 

And here is the map!

Example Map