Last active
May 23, 2026 23:07
-
-
Save walkerke/c4ae228039259fa2759a4d43e7039372 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Day 27: Boundaries - World Time Zones | |
| # The invisible lines that govern our daily lives | |
| # Uses current IANA time zone rules, including daylight saving time at render time. | |
| library(mapgl) | |
| library(sf) | |
| library(dplyr) | |
| library(lubridate) | |
| tz_release <- "2026b" | |
| # "now" is lighter and groups places by current/future clock behavior. | |
| # Use "1970" if you want the finer historical IANA zones, like Indiana's legacy intricacies. | |
| tz_scope <- "now" | |
| # Xinjiang has overlapping official China time (UTC+8) and local Urumqi time (UTC+6) usage. | |
| # Use "dual" to show both in the popup, "official" for Beijing/China Standard Time, | |
| # "local" for Urumqi/Xinjiang Time, or "both" to preserve the source overlap. | |
| xinjiang_policy <- "dual" | |
| tz_url <- paste0( | |
| "https://github.com/evansiroky/timezone-boundary-builder/releases/download/", | |
| tz_release, | |
| "/timezones-with-oceans-", | |
| tz_scope, | |
| ".geojson.zip" | |
| ) | |
| utc_now <- with_tz(Sys.time(), "UTC") | |
| user_tz <- Sys.timezone() | |
| user_time <- with_tz(utc_now, user_tz) | |
| read_timezone_boundaries <- function(url) { | |
| temp_zip <- tempfile(fileext = ".zip") | |
| temp_dir <- tempfile("timezone-boundaries-") | |
| dir.create(temp_dir) | |
| on.exit(unlink(c(temp_zip, temp_dir), recursive = TRUE), add = TRUE) | |
| download.file(url, temp_zip, mode = "wb", quiet = TRUE) | |
| unzip(temp_zip, exdir = temp_dir) | |
| geojson <- list.files(temp_dir, pattern = "\\.json$", full.names = TRUE) | |
| if (length(geojson) != 1) { | |
| stop("Expected exactly one GeoJSON file in timezone boundary archive.") | |
| } | |
| st_read(geojson, quiet = TRUE) | |
| } | |
| offset_minutes <- function(time, tz) { | |
| if (is.na(tz) || !tz %in% OlsonNames()) { | |
| return(NA_integer_) | |
| } | |
| offset <- format(time, tz = tz, format = "%z") | |
| sign <- ifelse(substr(offset, 1, 1) == "-", -1L, 1L) | |
| hours <- as.integer(substr(offset, 2, 3)) | |
| minutes <- as.integer(substr(offset, 4, 5)) | |
| sign * (hours * 60L + minutes) | |
| } | |
| offset_label <- function(minutes) { | |
| if (is.na(minutes)) { | |
| return("UTC unknown") | |
| } | |
| if (minutes == 0) { | |
| return("UTC") | |
| } | |
| sign <- ifelse(minutes > 0, "+", "-") | |
| abs_minutes <- abs(minutes) | |
| hours <- abs_minutes %/% 60L | |
| mins <- abs_minutes %% 60L | |
| if (mins == 0) { | |
| paste0("UTC", sign, hours) | |
| } else { | |
| paste0("UTC", sign, hours, ":", sprintf("%02d", mins)) | |
| } | |
| } | |
| format_time_in_tz <- function(time, tz, format) { | |
| if (is.na(tz) || !tz %in% OlsonNames()) { | |
| return(NA_character_) | |
| } | |
| format(time, tz = tz, format = format) | |
| } | |
| resolve_xinjiang_overlap <- function(zones, policy = c("dual", "official", "local", "both")) { | |
| policy <- match.arg(policy) | |
| zones$alternate_tzid <- NA_character_ | |
| zones$zone_label_override <- NA_character_ | |
| zones$zone_note <- NA_character_ | |
| if (policy == "both") { | |
| return(zones) | |
| } | |
| # In the "now" file, Asia/Manila represents current UTC+8 behavior, including China. | |
| # Asia/Dhaka represents current UTC+6 behavior, including local Xinjiang/Urumqi time. | |
| official_tz <- "Asia/Manila" | |
| local_tz <- "Asia/Dhaka" | |
| if (!all(c(official_tz, local_tz) %in% zones$tzid)) { | |
| return(zones) | |
| } | |
| old_s2 <- sf_use_s2() | |
| on.exit(suppressMessages(sf_use_s2(old_s2)), add = TRUE) | |
| suppressMessages(sf_use_s2(FALSE)) | |
| zones <- st_make_valid(zones) | |
| official_idx <- which(zones$tzid == official_tz) | |
| local_idx <- which(zones$tzid == local_tz) | |
| zones_work <- st_make_valid(st_transform(zones, 6933)) | |
| overlap <- suppressMessages(suppressWarnings(st_intersection( | |
| st_geometry(zones_work[official_idx, ]), | |
| st_geometry(zones_work[local_idx, ]) | |
| ))) | |
| if (length(overlap) == 0) { | |
| return(zones) | |
| } | |
| overlap <- suppressMessages(suppressWarnings( | |
| st_collection_extract(st_make_valid(st_union(overlap)), "POLYGON") | |
| )) | |
| xinjiang_extent <- st_as_sfc(st_bbox( | |
| c(xmin = 73, ymin = 34, xmax = 97, ymax = 50), | |
| crs = st_crs(zones) | |
| )) | |
| xinjiang_extent <- st_transform(xinjiang_extent, st_crs(zones_work)) | |
| overlap <- suppressMessages(suppressWarnings(st_intersection(overlap, xinjiang_extent))) | |
| trim_zone <- function(zone_idx) { | |
| trimmed <- suppressMessages(suppressWarnings(st_difference(st_geometry(zones_work[zone_idx, ]), overlap))) | |
| trimmed <- suppressMessages(suppressWarnings(st_collection_extract(st_make_valid(trimmed), "POLYGON"))) | |
| trimmed <- suppressMessages(suppressWarnings(st_transform(st_union(trimmed), st_crs(zones)))) | |
| st_geometry(zones)[zone_idx] <<- trimmed | |
| } | |
| if (policy == "official") { | |
| trim_zone(local_idx) | |
| return(zones) | |
| } | |
| if (policy == "local") { | |
| trim_zone(official_idx) | |
| return(zones) | |
| } | |
| xinjiang <- zones[official_idx, ] | |
| st_geometry(xinjiang) <- suppressMessages(suppressWarnings(st_transform(st_union(overlap), st_crs(zones)))) | |
| xinjiang$alternate_tzid <- local_tz | |
| xinjiang$zone_label_override <- "Xinjiang / Urumqi" | |
| xinjiang$zone_note <- "Official China time and local Urumqi time are both shown." | |
| zones <- rbind(zones, xinjiang) | |
| zones | |
| } | |
| timezones <- read_timezone_boundaries(tz_url) | |
| timezones <- resolve_xinjiang_overlap(timezones, xinjiang_policy) | |
| valid_tz <- timezones$tzid %in% OlsonNames() | |
| if (any(!valid_tz)) { | |
| warning( | |
| "Some boundary time zone IDs are not available in this R/system tz database: ", | |
| paste(timezones$tzid[!valid_tz], collapse = ", ") | |
| ) | |
| } | |
| timezones <- timezones |> | |
| mutate( | |
| offset_minutes = vapply(tzid, function(tz) offset_minutes(utc_now, tz), integer(1)), | |
| utc_offset = offset_minutes / 60, | |
| current_time = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%H:%M"), character(1)), | |
| time_ampm = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%I:%M %p"), character(1)), | |
| date_display = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%b %d"), character(1)), | |
| zone_abbr = vapply(tzid, function(tz) format_time_in_tz(utc_now, tz, "%Z"), character(1)), | |
| offset_label = vapply(offset_minutes, offset_label, character(1)), | |
| tooltip_title = if_else(is.na(current_time), "Time unavailable", current_time), | |
| alternate_time = vapply(alternate_tzid, function(tz) format_time_in_tz(utc_now, tz, "%H:%M"), character(1)), | |
| alternate_ampm = vapply(alternate_tzid, function(tz) format_time_in_tz(utc_now, tz, "%I:%M %p"), character(1)), | |
| alternate_abbr = vapply(alternate_tzid, function(tz) format_time_in_tz(utc_now, tz, "%Z"), character(1)), | |
| alternate_offset = vapply( | |
| vapply(alternate_tzid, function(tz) offset_minutes(utc_now, tz), integer(1)), | |
| offset_label, | |
| character(1) | |
| ), | |
| primary_time_label = if_else(is.na(alternate_tzid), "Local time", "Official China time"), | |
| alternate_label = if_else( | |
| is.na(alternate_tzid), | |
| "", | |
| paste0( | |
| "<div style='margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.14);'>", | |
| "<div style='font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.4px;'>Local Urumqi Time</div>", | |
| "<div style='font-size: 20px; font-weight: 700; color: #fff;'>", | |
| alternate_time, | |
| "</div>", | |
| "<div style='font-size: 12px; color: #888;'>", | |
| alternate_ampm, | |
| " ", | |
| alternate_abbr, | |
| " / ", | |
| alternate_offset, | |
| "</div>", | |
| "</div>" | |
| ) | |
| ), | |
| note_label = if_else( | |
| is.na(zone_note), | |
| "", | |
| paste0("<div style='font-size: 11px; color: #777; margin-top: 8px; max-width: 220px;'>", zone_note, "</div>") | |
| ), | |
| zone_label = coalesce(zone_label_override, gsub("_", " ", tzid)), | |
| is_user_tz = tzid == user_tz | |
| ) | |
| xinjiang_timezones <- timezones |> | |
| filter(!is.na(alternate_tzid)) | |
| timezones <- timezones |> | |
| filter(is.na(alternate_tzid)) | |
| user_offset_minutes <- offset_minutes(utc_now, user_tz) | |
| user_offset_label <- offset_label(user_offset_minutes) | |
| user_zone <- timezones |> | |
| filter(is_user_tz) | |
| if (nrow(user_zone) > 0) { | |
| user_bbox <- st_bbox(user_zone) | |
| user_center_lon <- mean(c(user_bbox[["xmin"]], user_bbox[["xmax"]])) | |
| user_center_lat <- mean(c(user_bbox[["ymin"]], user_bbox[["ymax"]])) | |
| user_center_lat <- max(min(user_center_lat, 55), -55) | |
| } else { | |
| user_center_lon <- (user_offset_minutes / 60) * 15 | |
| user_center_lat <- 20 | |
| } | |
| cat("Your timezone:", user_tz, "\n") | |
| cat("Your current time:", format(user_time, "%Y-%m-%d %H:%M:%S %Z"), "\n") | |
| cat("UTC offset:", user_offset_label, "\n") | |
| cat("Timezone boundary release:", tz_release, "\n") | |
| cat("Timezone boundary scope:", tz_scope, "\n") | |
| cat("Xinjiang policy:", xinjiang_policy, "\n") | |
| # Time zone color palette - dawn to dusk gradient | |
| tz_palette <- c( | |
| "#1e3a5f", # UTC-12 | |
| "#2d4a6f", # UTC-10 | |
| "#3d5a7f", # UTC-8 | |
| "#5a7a9f", # UTC-6 | |
| "#7a9abf", # UTC-4 | |
| "#9abadf", # UTC-2 | |
| "#f0f4f8", # UTC | |
| "#ffe4b5", # UTC+2 | |
| "#ffc987", # UTC+4 | |
| "#ffaa5c", # UTC+6 | |
| "#ff8533", # UTC+8 | |
| "#ff5500", # UTC+10 | |
| "#cc3300", # UTC+12 | |
| "#8f2200" # UTC+14 | |
| ) | |
| generated_label <- format(user_time, "%b %d, %Y %H:%M %Z") | |
| info_html <- paste0( | |
| "<div style='font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; ", | |
| "background: rgba(20, 20, 30, 0.95); padding: 16px 20px; border-radius: 8px; ", | |
| "border: 1px solid rgba(255, 255, 255, 0.1); max-width: 330px;'>", | |
| "<div style='font-size: 18px; font-weight: 700; color: #fff; margin-bottom: 10px;'>", | |
| "World Time Zones</div>", | |
| "<div style='background: rgba(255, 200, 100, 0.15); border: 1px solid rgba(255, 200, 100, 0.4); ", | |
| "border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;'>", | |
| "<div style='font-size: 11px; color: #ffc864; text-transform: uppercase; letter-spacing: 0.5px;'>Your Time</div>", | |
| "<div style='font-size: 24px; font-weight: 700; color: #fff;'>", | |
| format(user_time, "%H:%M"), | |
| "</div>", | |
| "<div style='font-size: 12px; color: #aaa;'>", | |
| user_tz, | |
| " (", | |
| user_offset_label, | |
| ")", | |
| "</div>", | |
| "</div>", | |
| "<div style='font-size: 12px; color: #888; line-height: 1.5;'>", | |
| "Hover over any zone to see the local time calculated from IANA time zone rules, including daylight saving where applicable.</div>", | |
| "<div style='font-size: 10px; color: #555; margin-top: 10px; line-height: 1.4;'>", | |
| "Calculated at ", | |
| generated_label, | |
| ". Current-equivalent IANA zones from timezone-boundary-builder ", | |
| tz_release, | |
| " / OpenStreetMap. Xinjiang: ", | |
| case_when( | |
| xinjiang_policy == "official" ~ "official China time", | |
| xinjiang_policy == "local" ~ "local Urumqi time", | |
| xinjiang_policy == "dual" ~ "both official China time and local Urumqi time shown", | |
| TRUE ~ "source overlap preserved" | |
| ), | |
| ".</div>", | |
| "</div>" | |
| ) | |
| maplibre( | |
| style = carto_style("dark-matter"), | |
| center = c(user_center_lon, user_center_lat), | |
| zoom = 2 | |
| ) |> | |
| add_fill_layer( | |
| id = "timezone-fill", | |
| source = timezones, | |
| fill_color = interpolate( | |
| column = "utc_offset", | |
| values = c(-12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14), | |
| stops = tz_palette, | |
| na_color = "#333333" | |
| ), | |
| fill_opacity = 0.72, | |
| hover_options = list( | |
| fill_opacity = 0.95 | |
| ), | |
| tooltip = concat( | |
| "<div style='background: #1a1a2e; padding: 14px 18px; border-radius: 8px; ", | |
| "border: 1px solid rgba(255, 255, 255, 0.2); font-family: system-ui;'>", | |
| "<div style='font-size: 32px; font-weight: 700; color: #fff;'>", | |
| get_column("tooltip_title"), | |
| "</div>", | |
| "<div style='font-size: 13px; color: #888; margin-bottom: 8px;'>", | |
| get_column("primary_time_label"), | |
| ": ", | |
| get_column("time_ampm"), | |
| " ", | |
| get_column("zone_abbr"), | |
| "</div>", | |
| "<div style='font-size: 13px; color: #aaa; margin-bottom: 8px;'>", | |
| get_column("date_display"), | |
| "</div>", | |
| "<div style='font-size: 14px; font-weight: 600; color: #ffc864;'>", | |
| get_column("offset_label"), | |
| "</div>", | |
| get_column("alternate_label"), | |
| "<div style='font-size: 11px; color: #666; margin-top: 6px;'>", | |
| get_column("zone_label"), | |
| "</div>", | |
| get_column("note_label"), | |
| "</div>" | |
| ) | |
| ) |> | |
| add_fill_layer( | |
| id = "timezone-xinjiang-dual", | |
| source = xinjiang_timezones, | |
| fill_color = interpolate( | |
| column = "utc_offset", | |
| values = c(-12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14), | |
| stops = tz_palette, | |
| na_color = "#333333" | |
| ), | |
| fill_opacity = 0.82, | |
| hover_options = list( | |
| fill_opacity = 0.98 | |
| ), | |
| tooltip = concat( | |
| "<div style='background: #1a1a2e; padding: 14px 18px; border-radius: 8px; ", | |
| "border: 1px solid rgba(255, 255, 255, 0.2); font-family: system-ui;'>", | |
| "<div style='font-size: 32px; font-weight: 700; color: #fff;'>", | |
| get_column("tooltip_title"), | |
| "</div>", | |
| "<div style='font-size: 13px; color: #888; margin-bottom: 8px;'>", | |
| get_column("primary_time_label"), | |
| ": ", | |
| get_column("time_ampm"), | |
| " ", | |
| get_column("zone_abbr"), | |
| "</div>", | |
| "<div style='font-size: 13px; color: #aaa; margin-bottom: 8px;'>", | |
| get_column("date_display"), | |
| "</div>", | |
| "<div style='font-size: 14px; font-weight: 600; color: #ffc864;'>", | |
| get_column("offset_label"), | |
| "</div>", | |
| get_column("alternate_label"), | |
| "<div style='font-size: 11px; color: #666; margin-top: 6px;'>", | |
| get_column("zone_label"), | |
| "</div>", | |
| get_column("note_label"), | |
| "</div>" | |
| ) | |
| ) |> | |
| add_line_layer( | |
| id = "timezone-lines", | |
| source = timezones, | |
| line_color = "#ffffff", | |
| line_width = interpolate( | |
| property = "zoom", | |
| values = c(1, 4, 8), | |
| stops = c(0.25, 0.8, 1.5) | |
| ), | |
| line_opacity = 0.35 | |
| ) |> | |
| add_control( | |
| html = info_html, | |
| position = "top-left" | |
| ) |> | |
| add_legend( | |
| legend_title = "Current UTC Offset", | |
| values = c("-12", "-6", "0", "+6", "+14"), | |
| colors = c("#1e3a5f", "#5a7a9f", "#f0f4f8", "#ffaa5c", "#8f2200"), | |
| type = "continuous", | |
| position = "bottom-left" | |
| ) |> | |
| add_navigation_control(position = "top-right") |> | |
| add_fullscreen_control(position = "top-right") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment