# Packages
pkgs <- c("sf", "ggplot2", "rnaturalearth", "rnaturalearthdata",
"igraph", "dplyr", "ggspatial", "osmextract", "httr", "jsonlite")
for (p in pkgs) {
if (!requireNamespace(p, quietly = TRUE)) install.packages(p)
}
library(sf); library(ggplot2)
## Warning: package 'sf' was built under R version 4.3.3
## Linking to GEOS 3.11.2, GDAL 3.8.2, PROJ 9.3.1; sf_use_s2() is TRUE
library(rnaturalearth); library(rnaturalearthdata)
## Warning: package 'rnaturalearthdata' was built under R version 4.3.3
##
## Attaching package: 'rnaturalearthdata'
## The following object is masked from 'package:rnaturalearth':
##
## countries110
library(igraph); library(dplyr)
## Warning: package 'igraph' was built under R version 4.3.3
##
## Attaching package: 'igraph'
## The following objects are masked from 'package:stats':
##
## decompose, spectrum
## The following object is masked from 'package:base':
##
## union
## Warning: package 'dplyr' was built under R version 4.3.3
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:igraph':
##
## as_data_frame, groups, union
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(ggspatial); library(osmextract)
## Data (c) OpenStreetMap contributors, ODbL 1.0. https://www.openstreetmap.org/copyright.
## Check the package website, https://docs.ropensci.org/osmextract/, for more details.
library(httr); library(jsonlite)
## Warning: package 'httr' was built under R version 4.3.3
## Warning: package 'jsonlite' was built under R version 4.3.3
set.seed(42)
utm32 <- st_crs(32632)
# 1.Denmark boundary
denmark_wgs <- ne_countries(country = "Denmark", scale = "medium",
returnclass = "sf") |> st_geometry()
denmark_utm <- st_transform(denmark_wgs, utm32)
# 2. Copenhagen distribution centre
cph_wgs <- st_sfc(st_point(c(12.5683, 55.6761)), crs = 4326)
cph_utm <- st_transform(cph_wgs, utm32) |> st_coordinates()
depot <- data.frame(name = "Copenhagen",
X = cph_utm[1,"X"],
Y = cph_utm[1,"Y"],
depot = TRUE,
lon = 12.5683,
lat = 55.6761)
# 100 towns within Denmark
message("Generating 100 towns...")
## Generating 100 towns...
bbox <- st_bbox(denmark_utm)
towns <- data.frame()
while (nrow(towns) < 100) {
xs <- runif(3000, bbox["xmin"], bbox["xmax"])
ys <- runif(3000, bbox["ymin"], bbox["ymax"])
pts <- st_as_sf(data.frame(X = xs, Y = ys), coords = c("X","Y"), crs = utm32)
ok <- st_within(pts, st_union(denmark_utm), sparse = FALSE)[,1]
cr <- st_coordinates(pts[ok,])
towns <- rbind(towns, data.frame(X = cr[,"X"], Y = cr[,"Y"]))
}
towns <- towns[1:100,]
# Convert UTM towns back to WGS84 lon/lat for OSRM
towns_sf <- st_as_sf(towns, coords = c("X","Y"), crs = utm32)
towns_wgs <- st_transform(towns_sf, 4326)
towns_ll <- st_coordinates(towns_wgs)
towns$name <- paste0("Town_", sprintf("%03d", 1:100))
towns$depot <- FALSE
towns$lon <- towns_ll[,"X"]
towns$lat <- towns_ll[,"Y"]
# 4. All nodes
all_nodes <- bind_rows(depot, towns)
rownames(all_nodes) <- NULL
n_nodes <- nrow(all_nodes) # 101
message(sprintf("Total stops: %d (depot + 100 towns)", n_nodes))
## Total stops: 101 (depot + 100 towns)
# 5. Road-distance matrix via OSRM public API
message("Computing road-distance matrix via OSRM (chunked)...")
## Computing road-distance matrix via OSRM (chunked)...
message(" This will take ~2-4 minutes (HTTP calls to router.project-osrm.org)...")
## This will take ~2-4 minutes (HTTP calls to router.project-osrm.org)...
# Build coordinate string: "lon,lat;lon,lat;..."
coords_str <- paste(
sprintf("%.6f,%.6f", all_nodes$lon, all_nodes$lat),
collapse = ";"
)
# Single OSRM /table call for all 101 x 101 pairs
osrm_url <- paste0(
"http://router.project-osrm.org/table/v1/driving/",
coords_str,
"?annotations=distance"
)
message(" Sending OSRM table request...")
## Sending OSRM table request...
resp <- tryCatch(
GET(osrm_url, timeout(120)),
error = function(e) {
message(" OSRM request failed: ", conditionMessage(e))
NULL
}
)
if (!is.null(resp) && status_code(resp) == 200) {
osrm_data <- fromJSON(content(resp, "text", encoding = "UTF-8"))
road_dist_mat <- matrix(
unlist(osrm_data$distances),
nrow = n_nodes,
ncol = n_nodes,
byrow = TRUE
)
message(" OSRM distance matrix complete.")
} else {
# Straight line Euclidean distances in UTM metres
message(" OSRM unavailable — using straight-line distances as fallback.")
coords_mat <- as.matrix(all_nodes[, c("X","Y")])
road_dist_mat <- as.matrix(dist(coords_mat))
}
## OSRM unavailable — using straight-line distances as fallback.
# Replace NA / Inf with large penalty
max_d <- max(road_dist_mat[is.finite(road_dist_mat) & road_dist_mat > 0],
na.rm = TRUE)
road_dist_mat[!is.finite(road_dist_mat) | is.na(road_dist_mat)] <- max_d * 10
diag(road_dist_mat) <- 0
# 6. MST-DFS tour
message("Solving TSP via MST-DFS...")
## Solving TSP via MST-DFS...
g_tsp <- graph_from_adjacency_matrix(road_dist_mat, mode = "undirected",
weighted = TRUE, diag = FALSE)
mst_g <- mst(g_tsp)
dfs_res <- dfs(mst_g, root = 1, neimode = "all", unreachable = FALSE)
## Warning: The `neimode` argument of `dfs()` is deprecated as of igraph 1.3.0.
## ℹ Please use the `mode` argument instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
tour <- c(as.integer(dfs_res$order), 1L)
# 7. 2-opt improvement
message("Applying 2-opt improvement...")
## Applying 2-opt improvement...
two_opt <- function(tour, dm) {
n <- length(tour) - 1L
improved <- TRUE
while (improved) {
improved <- FALSE
for (i in 1:(n - 2L)) {
for (j in (i + 2L):n) {
if (i == 1L && j == n) next
if ((dm[tour[i], tour[j]] + dm[tour[i+1L], tour[j+1L]]) <
(dm[tour[i], tour[i+1L]] + dm[tour[j], tour[j+1L]]) - 1e-6) {
tour[(i+1L):j] <- rev(tour[(i+1L):j])
improved <- TRUE
}
}
}
}
tour
}
tour <- two_opt(tour, road_dist_mat)
# 8. Stop numbering
unique_tour <- tour[1:n_nodes]
stop_num <- integer(n_nodes)
for (i in seq_along(unique_tour)) stop_num[unique_tour[i]] <- i - 1L
all_nodes$stop_num <- stop_num
all_nodes$map_label <- ifelse(all_nodes$depot, "Copenhagen",
as.character(stop_num))
tour_length_km <- round(
sum(road_dist_mat[cbind(head(tour,-1), tail(tour,-1))]) / 1000, 1)
cat(sprintf("\nTour length: %.1f km\n", tour_length_km))
##
## Tour length: 1978.1 km
# 9. Route geometry via OSRM /route
message("Fetching road geometry for each tour leg from OSRM...")
## Fetching road geometry for each tour leg from OSRM...
get_osrm_route <- function(lon1, lat1, lon2, lat2) {
url <- sprintf(
"http://router.project-osrm.org/route/v1/driving/%.6f,%.6f;%.6f,%.6f?geometries=geojson&overview=full",
lon1, lat1, lon2, lat2
)
resp <- tryCatch(GET(url, timeout(30)), error = function(e) NULL)
if (is.null(resp) || status_code(resp) != 200) return(NULL)
parsed <- fromJSON(content(resp, "text", encoding = "UTF-8"),
simplifyVector = FALSE)
if (parsed$code != "Ok") return(NULL)
coords <- parsed$routes[[1]]$geometry$coordinates
mat <- do.call(rbind, lapply(coords, function(c) c(c[[1]], c[[2]])))
if (nrow(mat) < 2) return(NULL)
st_linestring(mat)
}
n_legs <- length(tour) - 1
leg_geoms <- vector("list", n_legs)
for (i in seq_len(n_legs)) {
a <- tour[i]; b <- tour[i + 1]
geom <- get_osrm_route(
all_nodes$lon[a], all_nodes$lat[a],
all_nodes$lon[b], all_nodes$lat[b]
)
if (is.null(geom)) {
# straight-line fallback in WGS84
geom <- st_linestring(rbind(
c(all_nodes$lon[a], all_nodes$lat[a]),
c(all_nodes$lon[b], all_nodes$lat[b])
))
}
leg_geoms[[i]] <- geom
if (i %% 20 == 0) message(sprintf(" Leg %d / %d done", i, n_legs))
}
## Leg 20 / 101 done
## Leg 40 / 101 done
## Leg 60 / 101 done
## Leg 80 / 101 done
## Leg 100 / 101 done
route_sfc <- st_sfc(leg_geoms, crs = 4326) |>
st_transform(utm32)
route_sf <- st_sf(geometry = route_sfc)
message(" Route geometry complete.")
## Route geometry complete.
# 10. Download road layer for background display
message("Downloading road background layer from Geofabrik...")
## Downloading road background layer from Geofabrik...
options(timeout = 600)
road_display_types <- c("motorway","trunk","primary","secondary")
roads_bg <- tryCatch({
raw <- oe_get(
place = "Denmark",
layer = "lines",
provider = "geofabrik",
force_download = FALSE,
force_vectortranslate = FALSE,
query = "SELECT * FROM lines WHERE highway IS NOT NULL",
max_file_size = 5e8,
quiet = TRUE
)
raw |>
filter(highway %in% road_display_types) |>
filter(as.character(st_geometry_type(geometry)) == "LINESTRING") |>
filter(!st_is_empty(geometry)) |>
st_transform(utm32) |>
select(highway, geometry)
}, error = function(e) {
message(" Road background unavailable: ", conditionMessage(e))
NULL
})
## Road background unavailable: The download operation was aborted. If this was not intentional, you may want to increase the timeout for internet operations to a value >= 300 by using options(timeout = ...) before re-running this function. We also suggest you to remove the partially downloaded file by running the following code (possibly in a new R session): file.remove("C:/Users/User/AppData/Local/Temp/Rtmpeo6E5S/geofabrik_denmark-latest.osm.pbf")
# 11. Plotting data frames
towns_df <- all_nodes[!all_nodes$depot,]
depot_df <- all_nodes[ all_nodes$depot,]
# 12. Build map
message("Building map...")
## Building map...
p <- ggplot() +
# Country – white fill, black border
geom_sf(data = denmark_utm, fill = "white",
colour = "black", linewidth = 0.5) +
# Background roads
{ if (!is.null(roads_bg))
geom_sf(data = roads_bg, colour = "#bbbbbb",
linewidth = 0.12, alpha = 0.7)
else list() } +
# TSP route – red
geom_sf(data = route_sf, colour = "#CC0000",
linewidth = 0.55, alpha = 0.92) +
# Town circles – blue
geom_point(data = towns_df, aes(x = X, y = Y),
shape = 21, fill = "#2171b5",
colour = "black", size = 2.0, stroke = 0.4) +
# Copenhagen circle – green
geom_point(data = depot_df, aes(x = X, y = Y),
shape = 21, fill = "#238b45",
colour = "black", size = 4.5, stroke = 0.9) +
# Town stop numbers
geom_text(data = towns_df, aes(x = X, y = Y, label = map_label),
size = 1.7, colour = "black", hjust = 0,
nudge_x = 3200, check_overlap = TRUE) +
# Copenhagen label
geom_text(data = depot_df, aes(x = X, y = Y, label = map_label),
size = 3.2, colour = "black", fontface = "bold",
nudge_y = 16000, hjust = 0.5) +
annotation_scale(location = "bl", width_hint = 0.22,
unit_category = "metric") +
annotation_north_arrow(location = "tr",
style = north_arrow_fancy_orienteering()) +
labs(
title = "COCACOLA OPTIMAL DISTRIBUTION ROUTE IN DENMARK",
subtitle = sprintf(
"100 towns | Distribution Centre: Copenhagen | Road distance: %.1f km | UTM Zone 32N (EPSG:32632)",
tour_length_km),
x = "Easting", y = "Northing"
) +
theme_minimal(base_size = 10) +
theme(
plot.title = element_text(size = 11, face = "bold",
colour = "black", hjust = 0.5,
margin = margin(b = 4),
decoration = "underline"),
plot.subtitle = element_text(size = 7, colour = "black", hjust = 0.5),
plot.caption = element_blank(),
panel.background = element_rect(fill = "white", colour = NA),
plot.background = element_rect(fill = "white", colour = NA),
panel.grid.major = element_line(colour = "#cccccc", linewidth = 0.25),
axis.text = element_text(size = 7),
axis.title = element_text(size = 8)
)
## Warning in element_text(size = 11, face = "bold", colour = "black", hjust = 0.5, : `...` must be empty.
## ✖ Problematic argument:
## • decoration = "underline"
# 13. Show in RStudio Plots panel
print(p)

# 14. Save A4 Landscape PDF
pdf_dir <- "C:/Users/User/Desktop/CLASS NOTES/4.2 Notes/GIS Applications/Assignments/Assignment 5"
if (!dir.exists(pdf_dir)) dir.create(pdf_dir, recursive = TRUE)
pdf_file <- file.path(pdf_dir, "denmark_tsp_route.pdf")
pdf(pdf_file, width = 11.69, height = 8.27, paper = "a4r")
print(p)
dev.off()
## png
## 2
cat(sprintf("\nPDF saved to:\n %s\n", pdf_file))
##
## PDF saved to:
## C:/Users/User/Desktop/CLASS NOTES/4.2 Notes/GIS Applications/Assignments/Assignment 5/denmark_tsp_route.pdf
# 15. Tour order to console
cat("\n--- Tour order ---\n")
##
## --- Tour order ---
for (i in seq_along(unique_tour)) {
idx <- unique_tour[i]
cat(sprintf(" Stop %3d : %s\n", i - 1L,
ifelse(all_nodes$depot[idx], "Copenhagen (Depot)",
all_nodes$name[idx])))
}
## Stop 0 : Copenhagen (Depot)
## Stop 1 : Town_086
## Stop 2 : Town_032
## Stop 3 : Town_006
## Stop 4 : Town_071
## Stop 5 : Town_001
## Stop 6 : Town_030
## Stop 7 : Town_011
## Stop 8 : Town_087
## Stop 9 : Town_038
## Stop 10 : Town_096
## Stop 11 : Town_066
## Stop 12 : Town_023
## Stop 13 : Town_075
## Stop 14 : Town_094
## Stop 15 : Town_031
## Stop 16 : Town_095
## Stop 17 : Town_012
## Stop 18 : Town_058
## Stop 19 : Town_039
## Stop 20 : Town_062
## Stop 21 : Town_097
## Stop 22 : Town_033
## Stop 23 : Town_015
## Stop 24 : Town_056
## Stop 25 : Town_048
## Stop 26 : Town_057
## Stop 27 : Town_089
## Stop 28 : Town_084
## Stop 29 : Town_019
## Stop 30 : Town_003
## Stop 31 : Town_065
## Stop 32 : Town_014
## Stop 33 : Town_054
## Stop 34 : Town_002
## Stop 35 : Town_085
## Stop 36 : Town_041
## Stop 37 : Town_100
## Stop 38 : Town_064
## Stop 39 : Town_008
## Stop 40 : Town_025
## Stop 41 : Town_021
## Stop 42 : Town_093
## Stop 43 : Town_074
## Stop 44 : Town_020
## Stop 45 : Town_051
## Stop 46 : Town_042
## Stop 47 : Town_079
## Stop 48 : Town_035
## Stop 49 : Town_080
## Stop 50 : Town_068
## Stop 51 : Town_007
## Stop 52 : Town_092
## Stop 53 : Town_037
## Stop 54 : Town_026
## Stop 55 : Town_076
## Stop 56 : Town_098
## Stop 57 : Town_069
## Stop 58 : Town_059
## Stop 59 : Town_088
## Stop 60 : Town_083
## Stop 61 : Town_099
## Stop 62 : Town_013
## Stop 63 : Town_091
## Stop 64 : Town_044
## Stop 65 : Town_050
## Stop 66 : Town_052
## Stop 67 : Town_004
## Stop 68 : Town_063
## Stop 69 : Town_040
## Stop 70 : Town_045
## Stop 71 : Town_018
## Stop 72 : Town_061
## Stop 73 : Town_073
## Stop 74 : Town_072
## Stop 75 : Town_081
## Stop 76 : Town_078
## Stop 77 : Town_036
## Stop 78 : Town_047
## Stop 79 : Town_090
## Stop 80 : Town_049
## Stop 81 : Town_027
## Stop 82 : Town_070
## Stop 83 : Town_043
## Stop 84 : Town_082
## Stop 85 : Town_028
## Stop 86 : Town_022
## Stop 87 : Town_005
## Stop 88 : Town_016
## Stop 89 : Town_053
## Stop 90 : Town_024
## Stop 91 : Town_029
## Stop 92 : Town_009
## Stop 93 : Town_055
## Stop 94 : Town_060
## Stop 95 : Town_010
## Stop 96 : Town_046
## Stop 97 : Town_067
## Stop 98 : Town_077
## Stop 99 : Town_017
## Stop 100 : Town_034
cat(" --> Return to Copenhagen\n")
## --> Return to Copenhagen