--- 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)