---
# System prepended metadata

title: 如何用 R 語言計算周邊測站？Haversine 公式 + Leaflet 視覺化完整教學
tags: [R]

---

---
disqus: ahb0222
GA : G-VF9ZT413CG
---
> [color=#40f1ef][name=LHB阿好伯][:earth_africa:](https://www.facebook.com/LHB0222/)
>[time=Tue, Dec 16, 2025 8:39 AM]

[TOC]

在環境監測專案中，我們經常需要回答一個問題

「某個地點周圍有多少監測站？」這支 R 腳本正是為了解決這個需求而設計。

核心功能： 透過 Haversine 公式計算球面距離
將測站依據與目標點的距離分為三個等級，並以 Leaflet 繪製互動式地圖。

三圈分類邏輯：

🔴 紅色標記：內圈範圍內（預設 1 公里）
🔵 藍色標記：內圈外、外圈內（預設 1-2 公里）
🟢 綠色標記：外圈範圍外（超過 2 公里）

彈性設計： 只需調整程式開頭的 radius_inner 和 radius_outer 兩個變數

即可自訂篩選半徑，適用於不同場景的分析需求。

地圖呈現特色：
===
支援 OpenStreetMap、CartoDB、衛星影像三種底圖切換
內圈紅色實線、外圈藍色虛線清楚標示範圍
點擊標記可查看測站名稱、位置及精確距離
圖例自動統計各類別數量

這個工具特別適合用於選址評估、監測網絡覆蓋分析，或是快速掌握特定區域的空品感測器分布狀況。

![image](https://hackmd.io/_uploads/r1KMgBpzWg.png)

---


```r=
# 繪製測站地圖與半徑圓
# 使用 leaflet 建立互動式地圖

library(readr)
library(leaflet)
library(htmlwidgets)

# ============================================
# 可調整參數
# ============================================
radius_inner <- 1  # 內圈半徑（公里）- 紅色標記
radius_outer <- 2  # 外圈半徑（公里）- 藍色標記範圍
# 超過外圈半徑的為綠色標記

# 目標座標
target_lat <- 22.696152701092515
target_lon <- 120.2971322908144

# ============================================
# 讀取資料
# ============================================
df <- read_csv("20251212-0644.csv")

# Haversine 公式計算距離（公里）
haversine_distance <- function(lat1, lon1, lat2, lon2) {
  R <- 6371
  lat1_rad <- lat1 * pi / 180
  lat2_rad <- lat2 * pi / 180
  delta_lat <- (lat2 - lat1) * pi / 180
  delta_lon <- (lon2 - lon1) * pi / 180
  a <- sin(delta_lat / 2)^2 + cos(lat1_rad) * cos(lat2_rad) * sin(delta_lon / 2)^2
  c <- 2 * atan2(sqrt(a), sqrt(1 - a))
  return(R * c)
}

# 計算每個測站與目標點的距離
df$distance_km <- mapply(
  haversine_distance,
  target_lat, target_lon,
  df$latitude, df$longitude
)

# ============================================
# 分類測站（三種類別）
# ============================================
df$category <- ifelse(
  df$distance_km <= radius_inner, 
  "inner",      # 紅色：內圈內
  ifelse(
    df$distance_km <= radius_outer, 
    "middle",   # 藍色：內圈外、外圈內
    "outer"     # 綠色：外圈外
  )
)

# 統計
stations_inner <- sum(df$category == "inner")
stations_middle <- sum(df$category == "middle")
stations_outer <- sum(df$category == "outer")

cat("============================================\n")
cat("搜尋中心: (", target_lat, ", ", target_lon, ")\n", sep = "")
cat("內圈半徑:", radius_inner, "公里\n")
cat("外圈半徑:", radius_outer, "公里\n")
cat("============================================\n")
cat("🔴 ", radius_inner, "公里內測站數量: ", stations_inner, "\n", sep = "")
cat("🔵 ", radius_inner, "-", radius_outer, "公里內測站數量: ", stations_middle, "\n", sep = "")
cat("🟢 ", radius_outer, "公里外測站數量: ", stations_outer, "\n", sep = "")
cat("============================================\n")

# ============================================
# 建立 leaflet 地圖
# ============================================
map <- leaflet() %>%
  # 底圖
  addTiles(group = "OpenStreetMap") %>%
  addProviderTiles(providers$CartoDB.Positron, group = "CartoDB") %>%
  addProviderTiles(providers$Esri.WorldImagery, group = "衛星影像") %>%
  
  # 設定視圖中心為目標座標
  setView(lng = target_lon, lat = target_lat, zoom = 13) %>%
  
  # 繪製外圈半徑圓（藍色邊界）
  addCircles(
    lng = target_lon,
    lat = target_lat,
    radius = radius_outer * 1000,  # 轉換為公尺
    color = "#007BFF",
    weight = 2,
    fillColor = "#007BFF",
    fillOpacity = 0.05,
    dashArray = "5,5",
    popup = paste0(
      "<b>外圈範圍</b><br>",
      "半徑: ", radius_outer, " 公里<br>",
      "直徑: ", radius_outer * 2, " 公里<br>",
      "範圍內測站: ", stations_inner + stations_middle, " 個"
    ),
    group = "外圈範圍"
  ) %>%
  
  # 繪製內圈半徑圓（紅色邊界）
  addCircles(
    lng = target_lon,
    lat = target_lat,
    radius = radius_inner * 1000,  # 轉換為公尺
    color = "#DC3545",
    weight = 3,
    fillColor = "#DC3545",
    fillOpacity = 0.1,
    popup = paste0(
      "<b>內圈範圍</b><br>",
      "半徑: ", radius_inner, " 公里<br>",
      "直徑: ", radius_inner * 2, " 公里<br>",
      "範圍內測站: ", stations_inner, " 個"
    ),
    group = "內圈範圍"
  ) %>%
  
  # 標記目標中心點
  addMarkers(
    lng = target_lon,
    lat = target_lat,
    popup = paste0(
      "<b>🎯 搜尋中心點</b><br>",
      "緯度: ", round(target_lat, 6), "<br>",
      "經度: ", round(target_lon, 6), "<br>",
      "內圈: ", radius_inner, " 公里<br>",
      "外圈: ", radius_outer, " 公里"
    ),
    icon = makeIcon(
      iconUrl = "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png",
      iconWidth = 25, iconHeight = 41,
      iconAnchorX = 12, iconAnchorY = 41
    ),
    group = "中心點"
  ) %>%
  
  # 繪製內圈測站（紅色）
  addCircleMarkers(
    data = df[df$category == "inner", ],
    lng = ~longitude,
    lat = ~latitude,
    radius = 9,
    color = "#DC3545",
    fillColor = "#DC3545",
    fillOpacity = 0.9,
    weight = 2,
    popup = ~paste0(
      "<b>", thing_name, "</b><br>",
      "城市: ", city, "<br>",
      "鄉鎮: ", township, "<br>",
      "座標: (", round(latitude, 6), ", ", round(longitude, 6), ")<br>",
      "<span style='color: #DC3545; font-weight: bold;'>",
      "🔴 距離: ", round(distance_km, 3), " 公里 (", radius_inner, "km內)</span>"
    ),
    group = paste0(radius_inner, "km內測站")
  ) %>%
  
  # 繪製中圈測站（藍色）
  addCircleMarkers(
    data = df[df$category == "middle", ],
    lng = ~longitude,
    lat = ~latitude,
    radius = 7,
    color = "#007BFF",
    fillColor = "#007BFF",
    fillOpacity = 0.8,
    weight = 2,
    popup = ~paste0(
      "<b>", thing_name, "</b><br>",
      "城市: ", city, "<br>",
      "鄉鎮: ", township, "<br>",
      "座標: (", round(latitude, 6), ", ", round(longitude, 6), ")<br>",
      "<span style='color: #007BFF; font-weight: bold;'>",
      "🔵 距離: ", round(distance_km, 3), " 公里 (", radius_inner, "-", radius_outer, "km)</span>"
    ),
    group = paste0(radius_inner, "-", radius_outer, "km測站")
  ) %>%
  
  # 繪製外圈測站（綠色）
  addCircleMarkers(
    data = df[df$category == "outer", ],
    lng = ~longitude,
    lat = ~latitude,
    radius = 5,
    color = "#28A745",
    fillColor = "#28A745",
    fillOpacity = 0.6,
    weight = 1,
    popup = ~paste0(
      "<b>", thing_name, "</b><br>",
      "城市: ", city, "<br>",
      "鄉鎮: ", township, "<br>",
      "座標: (", round(latitude, 6), ", ", round(longitude, 6), ")<br>",
      "<span style='color: #28A745;'>",
      "🟢 距離: ", round(distance_km, 3), " 公里 (", radius_outer, "km外)</span>"
    ),
    group = paste0(radius_outer, "km外測站")
  ) %>%
  
  # 圖層控制
  addLayersControl(
    baseGroups = c("OpenStreetMap", "CartoDB", "衛星影像"),
    overlayGroups = c(
      "內圈範圍", 
      "外圈範圍", 
      "中心點", 
      paste0(radius_inner, "km內測站"),
      paste0(radius_inner, "-", radius_outer, "km測站"),
      paste0(radius_outer, "km外測站")
    ),
    options = layersControlOptions(collapsed = FALSE)
  ) %>%
  
  # 圖例
  addLegend(
    position = "bottomright",
    colors = c("#DC3545", "#007BFF", "#28A745"),
    labels = c(
      paste0("🔴 ", radius_inner, "km內 (", stations_inner, ")"),
      paste0("🔵 ", radius_inner, "-", radius_outer, "km (", stations_middle, ")"),
      paste0("🟢 ", radius_outer, "km外 (", stations_outer, ")")
    ),
    title = paste0("測站分布 (共", nrow(df), "站)"),
    opacity = 0.9
  ) %>%
  
  # 比例尺
  addScaleBar(position = "bottomleft", options = scaleBarOptions(imperial = FALSE))

# 儲存為 HTML 檔案
saveWidget(map, "stations_map.html", selfcontained = TRUE)

cat("\n✅ 地圖已儲存為 stations_map.html\n")
cat("總測站數:", nrow(df), "\n")
```

# 多點位計算

![image](https://hackmd.io/_uploads/rkzAGHpfWx.png)


```r=
# ============================================
# R語言實戰：用 Leaflet 打造空品測站距離篩選地圖
# 支援多點位清單，計算不重複站點數量
# ============================================

library(readr)
library(leaflet)
library(htmlwidgets)
library(dplyr)

# ============================================
# 可調整參數
# ============================================
radius_inner <- 1  # 內圈半徑（公里）- 紅色標記
radius_outer <- 2  # 外圈半徑（公里）- 藍色標記範圍

# ============================================
# 多點位清單設定
# 格式：data.frame(name, lat, lon)
# ============================================
target_points <- data.frame(
  name = c("點位A-鼓山區", "點位B-前鎮區", "點位C-左營區"),
  lat = c(22.696152701092515, 22.681366159432702, 22.690000),
  lon = c(120.2971322908144, 120.3790205926145, 120.295000)
)

# 點位顏色（可自訂，循環使用）
point_colors <- c("#E74C3C", "#9B59B6", "#F39C12", "#1ABC9C", "#3498DB")

# ============================================
# 讀取資料
# ============================================
df <- read_csv("20251212-0644.csv")

# Haversine 公式計算距離（公里）
haversine_distance <- function(lat1, lon1, lat2, lon2) {
  R <- 6371
  lat1_rad <- lat1 * pi / 180
  lat2_rad <- lat2 * pi / 180
  delta_lat <- (lat2 - lat1) * pi / 180
  delta_lon <- (lon2 - lon1) * pi / 180
  a <- sin(delta_lat / 2)^2 + cos(lat1_rad) * cos(lat2_rad) * sin(delta_lon / 2)^2
  c <- 2 * atan2(sqrt(a), sqrt(1 - a))
  return(R * c)
}

# ============================================
# 計算每個測站到所有點位的距離
# ============================================
for (i in 1:nrow(target_points)) {
  col_name <- paste0("dist_", i)
  df[[col_name]] <- mapply(
    haversine_distance,
    target_points$lat[i], target_points$lon[i],
    df$latitude, df$longitude
  )
}

# 計算每個測站到最近點位的距離
dist_cols <- paste0("dist_", 1:nrow(target_points))
df$min_distance <- apply(df[, dist_cols], 1, min)
df$nearest_point <- apply(df[, dist_cols], 1, which.min)
df$nearest_point_name <- target_points$name[df$nearest_point]

# ============================================
# 分類測站（依據最近點位的距離）
# ============================================
df$category <- ifelse(
  df$min_distance <= radius_inner, 
  "inner",
  ifelse(
    df$min_distance <= radius_outer, 
    "middle",
    "outer"
  )
)

# ============================================
# 統計分析
# ============================================
stations_inner <- sum(df$category == "inner")
stations_middle <- sum(df$category == "middle")
stations_outer <- sum(df$category == "outer")

# 不重複站點統計（內圈 + 中圈）
unique_stations_in_range <- df %>% 
  filter(category %in% c("inner", "middle")) %>%
  distinct(station_id, .keep_all = TRUE)

unique_count_inner <- df %>% filter(category == "inner") %>% distinct(station_id) %>% nrow()
unique_count_middle <- df %>% filter(category == "middle") %>% distinct(station_id) %>% nrow()
unique_count_total <- nrow(unique_stations_in_range)

# 各點位統計
cat("\n")
cat("╔══════════════════════════════════════════════════════════════╗\n")
cat("║          空品測站距離篩選分析報告                            ║\n")
cat("╠══════════════════════════════════════════════════════════════╣\n")
cat("║ 參數設定                                                     ║\n")
cat("╠══════════════════════════════════════════════════════════════╣\n")
cat(sprintf("║  內圈半徑: %s 公里 (紅色)                                    ║\n", radius_inner))
cat(sprintf("║  外圈半徑: %s 公里 (藍色)                                    ║\n", radius_outer))
cat(sprintf("║  點位數量: %s 個                                             ║\n", nrow(target_points)))
cat("╠══════════════════════════════════════════════════════════════╣\n")
cat("║ 點位清單                                                     ║\n")
cat("╠══════════════════════════════════════════════════════════════╣\n")

for (i in 1:nrow(target_points)) {
  # 計算該點位範圍內的站點數
  inner_count <- sum(df[[paste0("dist_", i)]] <= radius_inner)
  outer_count <- sum(df[[paste0("dist_", i)]] <= radius_outer & df[[paste0("dist_", i)]] > radius_inner)
  cat(sprintf("║  %d. %s\n", i, target_points$name[i]))
  cat(sprintf("║     座標: (%.6f, %.6f)\n", target_points$lat[i], target_points$lon[i]))
  cat(sprintf("║     %dkm內: %d站 | %d-%dkm: %d站\n", 
              radius_inner, inner_count, radius_inner, radius_outer, outer_count))
  cat("║\n")
}

cat("╠══════════════════════════════════════════════════════════════╣\n")
cat("║ 📊 不重複站點統計（所有點位合併計算）                        ║\n")
cat("╠══════════════════════════════════════════════════════════════╣\n")
cat(sprintf("║  🔴 %dkm內不重複站點:     %4d 個                            ║\n", radius_inner, unique_count_inner))
cat(sprintf("║  🔵 %d-%dkm內不重複站點:   %4d 個                            ║\n", radius_inner, radius_outer, unique_count_middle))
cat(sprintf("║  ══════════════════════════                                 ║\n"))
cat(sprintf("║  ✅ 總計不重複站點:       %4d 個                            ║\n", unique_count_total))
cat(sprintf("║  🟢 範圍外站點:           %4d 個                            ║\n", stations_outer))
cat("╠══════════════════════════════════════════════════════════════╣\n")
cat(sprintf("║  📍 測站總數:             %4d 個                            ║\n", nrow(df)))
cat("╚══════════════════════════════════════════════════════════════╝\n")

# ============================================
# 建立 leaflet 地圖
# ============================================

# 計算地圖中心點（所有點位的平均）
center_lat <- mean(target_points$lat)
center_lon <- mean(target_points$lon)

map <- leaflet() %>%
  # 底圖
  addTiles(group = "OpenStreetMap") %>%
  addProviderTiles(providers$CartoDB.Positron, group = "CartoDB") %>%
  addProviderTiles(providers$Esri.WorldImagery, group = "衛星影像") %>%
  
  # 設定視圖中心
  
  setView(lng = center_lon, lat = center_lat, zoom = 13)

# ============================================
# 為每個點位繪製圓圈和標記
# ============================================
for (i in 1:nrow(target_points)) {
  pt <- target_points[i, ]
  color <- point_colors[(i - 1) %% length(point_colors) + 1]
  
  # 計算該點位範圍內的站點數
  inner_count <- sum(df[[paste0("dist_", i)]] <= radius_inner)
  total_count <- sum(df[[paste0("dist_", i)]] <= radius_outer)
  
  # 繪製外圈
  map <- map %>%
    addCircles(
      lng = pt$lon,
      lat = pt$lat,
      radius = radius_outer * 1000,
      color = color,
      weight = 2,
      fillColor = color,
      fillOpacity = 0.03,
      dashArray = "5,5",
      popup = paste0(
        "<b>", pt$name, " - 外圈</b><br>",
        "半徑: ", radius_outer, " 公里<br>",
        "範圍內測站: ", total_count, " 個"
      ),
      group = "外圈範圍"
    )
  
  # 繪製內圈
  map <- map %>%
    addCircles(
      lng = pt$lon,
      lat = pt$lat,
      radius = radius_inner * 1000,
      color = color,
      weight = 3,
      fillColor = color,
      fillOpacity = 0.08,
      popup = paste0(
        "<b>", pt$name, " - 內圈</b><br>",
        "半徑: ", radius_inner, " 公里<br>",
        "範圍內測站: ", inner_count, " 個"
      ),
      group = "內圈範圍"
    )
  
  # 標記點位中心
  map <- map %>%
    addMarkers(
      lng = pt$lon,
      lat = pt$lat,
      popup = paste0(
        "<b>🎯 ", pt$name, "</b><br>",
        "緯度: ", round(pt$lat, 6), "<br>",
        "經度: ", round(pt$lon, 6), "<br>",
        "<hr style='margin:5px 0;'>",
        "🔴 ", radius_inner, "km內: ", inner_count, " 站<br>",
        "🔵 ", radius_inner, "-", radius_outer, "km: ", total_count - inner_count, " 站"
      ),
      icon = makeIcon(
        iconUrl = "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png",
        iconWidth = 25, iconHeight = 41,
        iconAnchorX = 12, iconAnchorY = 41
      ),
      group = "點位中心"
    )
}

# ============================================
# 繪製測站標記
# ============================================

# 內圈測站（紅色）
map <- map %>%
  addCircleMarkers(
    data = df[df$category == "inner", ],
    lng = ~longitude,
    lat = ~latitude,
    radius = 9,
    color = "#DC3545",
    fillColor = "#DC3545",
    fillOpacity = 0.9,
    weight = 2,
    popup = ~paste0(
      "<b>", thing_name, "</b><br>",
      "城市: ", city, "<br>",
      "鄉鎮: ", township, "<br>",
      "座標: (", round(latitude, 6), ", ", round(longitude, 6), ")<br>",
      "<hr style='margin:5px 0;'>",
      "<span style='color: #DC3545; font-weight: bold;'>",
      "🔴 最近點位: ", nearest_point_name, "<br>",
      "距離: ", round(min_distance, 3), " 公里</span>"
    ),
    group = paste0(radius_inner, "km內測站")
  )

# 中圈測站（藍色）
map <- map %>%
  addCircleMarkers(
    data = df[df$category == "middle", ],
    lng = ~longitude,
    lat = ~latitude,
    radius = 7,
    color = "#007BFF",
    fillColor = "#007BFF",
    fillOpacity = 0.8,
    weight = 2,
    popup = ~paste0(
      "<b>", thing_name, "</b><br>",
      "城市: ", city, "<br>",
      "鄉鎮: ", township, "<br>",
      "座標: (", round(latitude, 6), ", ", round(longitude, 6), ")<br>",
      "<hr style='margin:5px 0;'>",
      "<span style='color: #007BFF; font-weight: bold;'>",
      "🔵 最近點位: ", nearest_point_name, "<br>",
      "距離: ", round(min_distance, 3), " 公里</span>"
    ),
    group = paste0(radius_inner, "-", radius_outer, "km測站")
  )

# 外圈測站（綠色）
map <- map %>%
  addCircleMarkers(
    data = df[df$category == "outer", ],
    lng = ~longitude,
    lat = ~latitude,
    radius = 5,
    color = "#28A745",
    fillColor = "#28A745",
    fillOpacity = 0.6,
    weight = 1,
    popup = ~paste0(
      "<b>", thing_name, "</b><br>",
      "城市: ", city, "<br>",
      "鄉鎮: ", township, "<br>",
      "座標: (", round(latitude, 6), ", ", round(longitude, 6), ")<br>",
      "<hr style='margin:5px 0;'>",
      "<span style='color: #28A745;'>",
      "🟢 最近點位: ", nearest_point_name, "<br>",
      "距離: ", round(min_distance, 3), " 公里</span>"
    ),
    group = paste0(radius_outer, "km外測站")
  )

# ============================================
# 圖層控制與圖例
# ============================================
map <- map %>%
  addLayersControl(
    baseGroups = c("OpenStreetMap", "CartoDB", "衛星影像"),
    overlayGroups = c(
      "內圈範圍", 
      "外圈範圍", 
      "點位中心", 
      paste0(radius_inner, "km內測站"),
      paste0(radius_inner, "-", radius_outer, "km測站"),
      paste0(radius_outer, "km外測站")
    ),
    options = layersControlOptions(collapsed = FALSE)
  ) %>%
  
  # 圖例
  addLegend(
    position = "bottomright",
    colors = c("#DC3545", "#007BFF", "#28A745"),
    labels = c(
      paste0("🔴 ", radius_inner, "km內 (", unique_count_inner, " 不重複)"),
      paste0("🔵 ", radius_inner, "-", radius_outer, "km (", unique_count_middle, " 不重複)"),
      paste0("🟢 ", radius_outer, "km外 (", stations_outer, ")")
    ),
    title = paste0(
      "測站分布<br>",
      "<small>點位: ", nrow(target_points), " | ",
      "總站: ", nrow(df), "<br>",
      "<b>不重複: ", unique_count_total, "</b></small>"
    ),
    opacity = 0.9
  ) %>%
  
  # 比例尺
  addScaleBar(position = "bottomleft", options = scaleBarOptions(imperial = FALSE))

# ============================================
# 儲存地圖與結果
# ============================================
saveWidget(map, "stations_map_multipoint.html", selfcontained = TRUE)

# 輸出不重複站點清單
cat("\n📋 範圍內不重複站點清單:\n")
cat("─────────────────────────────────────────────────────────────\n")
unique_stations_in_range %>%
  select(station_id, thing_name, city, township, min_distance, nearest_point_name, category) %>%
  arrange(min_distance) %>%
  print(n = 50)

cat("\n✅ 地圖已儲存為 stations_map_multipoint.html\n")
```

🌟全文可以至下方連結觀看或是補充


全文分享至

https://www.facebook.com/LHB0222/

https://www.instagram.com/ahb0222/

有疑問想討論的都歡迎於下方留言

喜歡的幫我分享給所有的朋友 \o/

有所錯誤歡迎指教

# [:page_with_curl: 全部文章列表](https://hackmd.io/@LHB-0222/AllWritings)



![](https://i.imgur.com/nHEcVmm.jpg)


