RAS Map Builder
EN: A fully customizable, data-driven mapping platform to select districts, customize colors, design rodent absence ranges, and generate deployment maps.
ZH: 一個完全自訂化、由數據驅動的地圖平台,用於選定行政分區、自訂色彩、配置無鼠百分比區間,並生成最終部署的地圖網頁。
This design note outlines the complete system architecture, data flow, interface contracts, and styling rules of the customizable Rodent Activity Survey (RAS) Map Builder. It serves as a comprehensive guide for developers, users, and future maintainers.
本開發設計手冊詳細說明了鼠隻活動調查(RAS)地圖構建系統的整體架構、數據流向、配置合約與樣式規則,旨在為開發人員、終端用戶及未來的系統維護者提供一份完整的、獨立的參考文檔。
01Purpose / 目的
The Food and Environmental Hygiene Department (FEHD) conducts the Rodent Activity Survey (RAS). Thermal surveillance cameras are deployed across various districts in successive survey phases. Completed phases report a Rodent Absence Rate (RAR, 無鼠百分比) for each camera location — where a higher RAR indicates better rodent control conditions.
食物環境衞生署(食環署)進行鼠隻活動調查(RAS),在各區不同的調查階段部署熱能探測相機。已結束的調查階段會報告每部相機的「無鼠百分比」(RAR),RAR 愈高代表該選點的防鼠成效愈理想。
To support all 19 FEHD administrative districts without rewriting code, the mapping system uses a reusable, district-agnostic template driven entirely by a configuration object. Non-technical users interact with a browser-based builder tool to configure geographic parameters, design custom color spectra, define status badge colors, upload camera datasets, and generate a fully functional, self-contained map page.
為了在不修改代碼的情況下支持食環署全部 19 個行政區,本系統採用了一個與具體分區無關的通用地圖模板,地圖呈現完全由傳入的配置對象驅動。非技術管理人員只需通過網頁端 Builder(構建器) 介面選擇分區、自訂色彩頻譜與徽章、上傳相機數據,便能一鍵生成可部署的獨立地圖網頁。
02System design / 系統設計
The system separates build-time configuration (setting up a district map) from run-time client-side rendering (viewing the map). These roles map onto three distinct files in the root deployment directory:
系統在「構建時配置」與「運行時客戶端渲染」之間保持了清晰的分離。為防止配置參數分散,所有自訂樣式與相機坐標都直接捆綁在配置負載中。項目文件均放置於系統部署的根目錄中:
/ras_map_builder.html — Builder / 構建器
A self-contained tool with a 2-column input/preview layout, real-time map preview synchronization, and advanced color spectrum/department badge editors.
一個獨立的構建工具,配備雙欄輸入/預覽佈局、實時地圖預覽同步,以及先進的色彩頻譜與部門徽章編輯器。
/map_template.html — Template / 地圖模板
The dynamic map application, refactored to dynamically generate legends, marker colors, and popup badges based on parameters inside the injected config.
核心地圖應用,可根據注入的配置參數動態生成圖例、標記顏色及彈出窗徽章。
/index.html — Product / 地圖產品
The finished, district-specific map, produced by the builder. Opened (view-only) by end users.
由 Builder 生成的、特定分區的獨立地圖文件,供終端用戶查看。
The builder reads the template, replaces the single marker (/*__APP_CONFIG__*/) with a compiled, ASCII-escaped window.APP_CONFIG = {…}, and downloads the resulting file. The template adapts automatically to whatever configuration parameters are provided.
Builder 讀取地圖模板,將單個標記(/*__APP_CONFIG__*/)替換為編譯且經過 ASCII 轉義的 window.APP_CONFIG = {…},最後下載生成的文件。地圖模板會自動適應傳入的自訂參數。
03The APP_CONFIG contract / 配置合約
This configuration object is the entire interface between the builder and the template. It supports district details, phase datasets, custom color spectra, pending/no-data colors, and custom department status mappings. The following example highlights a configuration for Tai Po District:
此配置對象是 Builder 與地圖模板之間的唯一接口。它支持分區詳情、階段數據集、自訂色彩頻譜、待調查/無數據顏色以及自訂部門狀態徽章映射。以下以大埔區的配置為例:
window.APP_CONFIG = {
district: {
nameEN: "Tai Po",
nameZH: "大埔區",
foodCode: "95",
center: [22.4501, 114.1681],
zoom: 15
},
phases: [
{
label: "2026-1",
subtitleEN: "2026 1st Phase",
subtitleZH: "2026年第1階段",
colorMode: "rar",
isDefault: true,
rows: [
{
id: "TP_038", dept: "FEHD", serial: "38",
locType: "Side lane", street: "ON FU ROAD",
streetNo: "36", landmark: "Hung Wai Building",
lat: 22.44831, lng: 114.16558, risk: "H", rar: 73.43
}
]
}
],
pools: [],
gravidtraps: [ // Array of custom gravidtrap survey records
{
lat: 22.4512, lng: 114.1685, trapNo: "1", area: "Tai Po East",
fields: {
"Gravidtrap No.": "1", "Survey Area": "Tai Po East",
"Year": "2026", "Month": "Apr", "Address or Landmark": "Tai Po East Sports Centre",
"Managed by": "FEHD", "Set Date": "2026-04-28", "GI Date": "2026-05-05",
"No. of GI Adult": "2", "Latitude": "22.4512", "Longitude": "114.1685"
}
}
],
colors: {
pending: "#6b8e23", // Custom Pending color hex
noData: "#9e9e9e", // Custom No-data color hex
mosPositive: "#ff0000", // Default Positive gravidtrap color
mosNegative: "#ffcc02", // Default Negative gravidtrap color
mosAreas: { // Area-specific gravidtrap color overrides
"Tai Po East": { pos: "#e91e63", neg: "#8bc34a" }
},
bands: [ // Array of custom RAR range rules
{ min: 100, max: 100, color: "#ffcc02", label: "100%" },
{ min: 95, max: 99.99, color: "#ff6d00", label: "95% – 99.9%" },
{ min: 90, max: 94.99, color: "#c62828", label: "90% - 94%" },
{ min: 0, max: 89.99, color: "#6f1515", label: "< 90%" }
]
},
departments: { // Custom department prefix mappings
FEHD: "#c1b315",
LCSD: "#2E7D32",
HD: "#6A1B9A",
DSD: "#00796B"
}
};
18 Administrative Districts Mapping / 18 行政分區代碼對照表
The builder correlates districts in Hong Kong with their respective CSDI/FEHD food-licence district codes (stored as the NSEARCH01_EN property field in government datasets), default town center coordinates, and map zoom levels:
構建器建立了香港行政分區與 CSDI/食環署食物牌照分區代碼(對應政府數據集中的 NSEARCH01_EN 欄位值)之對照字典,並設定了默認市中心坐標及地圖縮放層級:
| District / 分區 | CSDI District Code / 分區代碼 | Default Lat/Lng / 預設經緯度 | Zoom / 縮放 |
|---|---|---|---|
| Eastern / 東區 | 11 | 22.2829, 114.2230 | 14 |
| Wan Chai / 灣仔區 | 12 | 22.2779, 114.1731 | 15 |
| Southern / 南區 | 15 | 22.2470, 114.1590 | 14 |
| Islands / 離島區 | 17 | 22.2090, 113.9430 | 12 |
| Central & Western / 中西區 | 18 | 22.2870, 114.1550 | 15 |
| Kwun Tong / 觀塘區 | 51 | 22.3130, 114.2260 | 15 |
| Kowloon City / 九龍城區 | 52 | 22.3280, 114.1910 | 15 |
| Wong Tai Sin / 黃大仙區 | 53 | 22.3420, 114.1936 | 15 |
| Yau Tsim / 油尖區 | 61 | 22.3050, 114.1690 | 15 |
| Mong Kok / 旺角區 | 62 | 22.3190, 114.1690 | 15 |
| Sham Shui Po / 深水埗區 | 63 | 22.3303, 114.1622 | 15 |
| Kwai Tsing / 葵青區 | 91 | 22.3570, 114.1300 | 14 |
| Tsuen Wan / 荃灣區 | 92 | 22.3710, 114.1140 | 14 |
| Tuen Mun / 屯門區 | 93 | 22.3910, 113.9770 | 14 |
| Yuen Long / 元朗區 | 94 | 22.4445, 114.0220 | 14 |
| Tai Po / 大埔區 | 95 | 22.4501, 114.1681 | 15 |
| North / 北區 | 96 | 22.4940, 114.1380 | 13 |
| Sha Tin / 沙田區 | 97 | 22.3820, 114.1880 | 14 |
| Sai Kung / 西貢區 | 98 | 22.3810, 114.2710 | 13 |
04CSV input templates / CSV 範本
Colleagues fill two spreadsheet templates, downloadable from the builder. Because coordinate parsing and department classification happen at validation time in the client browser, raw CSV contents are preserved in memory during session states. This allows the builder to dynamically re-parse all coordinates and map department prefixes immediately if the user modifies department definitions in the customization tabs.
工作人員需填寫兩個 CSV 數據範本(可從構建器下載)。由於坐標解析和部門分類是在客戶端瀏覽器驗證時進行的,因此 CSV 的原始內容會保留在瀏覽器內存中。這使得當用戶在自訂面板中修改部門前綴或徽章顏色規則時,Builder 能夠立即動態重新解析所有坐標並重繪前綴徽章,而無需重新上傳文件。
Camera template — one file per phase / 相機數據範本(每階段一份)
| Column / 欄位 | Req. / 必填 | Format / 格式與數值 |
|---|---|---|
| Camera ID | ✓ | Combined ID, e.g. TP_038, HD_TP_606, LCSD_TP_801混合相機 ID,包含部門前綴代碼與序列號。 |
| Location Type | – | free text (Side lane, RCP, Garden …) 位置類型描述(如後巷、垃圾收集站、花園)。 |
| Street | – | free text 街道名稱。 |
| Street No. | – | free text 門牌號碼。 |
| Building / Landmark | – | free text (Traditional Chinese OK) 大廈名稱或地標(支援繁體中文)。 |
| Latitude | ✓ | WGS84 decimal (~22.x) WGS84 十進制緯度。 |
| Longitude | ✓ | WGS84 decimal (~114.x) WGS84 十進制經度。 |
| Risk | – | H / M / L / N.A. 風險等級。 |
| RAR (%) | – | number 0–100, or blank. 無鼠百分比(0-100)。若留空,代表調查仍在進行中,相機在地圖上顯示為待調查顏色(預設橄欖綠色)。 |
Pool template — one file per district / 抽樣池範本(每區一份)
Columns: Serial, District Cam Ref., Location Type, Street, Street No., Building / Landmark, Lamp Post, Street Details, Latitude, Longitude. The pool layer is header-driven — latitude/longitude are found by column name, and every populated column appears dynamically in popup bubbles.
包含序列號、分區參照、位置類型、燈柱編號、經緯度等。抽樣池圖層由表頭驅動,系統會自動定位經緯度列,並將所有已填寫的屬性動態呈現在彈出窗中。
Gravidtrap template — one file per district / 誘蚊器數據範本(每區一份)
Columns: Gravidtrap No., Survey Area, Year, Month, Address or Landmark, Managed by, Set Date, GI Date, No. of GI Adult, Latitude, Longitude. Under this template, the Latitude and Longitude columns reside at the end of the sheet. The Year column is optional (a year code indicates positive trap rounds, while an empty year represents negative trap rounds, with date-fallback auto-resolving the round's actual year).
包含誘蚊器編號、調查分區、年份、月份、位置描述、管理單位、放置日期、檢查日期、成蚊數量、緯度及經度。其中經緯度放置在最後兩列,年份為選填(留空代表陰性,有填代表陽性,系統會通過日期自動解析年份以進行合併篩選)。
CSV Processing Implementations / CSV 處理邏輯
To avoid Chinese character corruption (e.g. from Excel Big5 encoding) and accommodate minor header differences, the builder uses the following helper logics:
為防止中文亂碼(例如 Excel 以 Big5 格式儲存時)並容許表頭的微小差異,構建器採用了以下處理邏輯:
// 1. Header normalization: forces lowercase and strips all non-alphanumeric characters
const norm = h => (h || "").toLowerCase().replace(/[^a-z0-9]/g, "");
// 2. Encoding detection ladder: tries UTF-8, Big5, and GB18030 sequentially, choosing the one with the fewest replacement symbols ()
function readCsv(file, cb) {
const reader = new FileReader();
reader.onload = e => {
const bytes = new Uint8Array(e.target.result);
const tryDec = enc => {
try { return new TextDecoder(enc).decode(bytes); } catch(_) { return null; }
};
const bad = s => (s.match(/\uFFFD/g) || []).length;
let text = tryDec('utf-8') || '';
if (bad(text) > 0) {
for (const enc of ['big5', 'gb18030']) {
const t = tryDec(enc);
if (t !== null && bad(t) < bad(text)) text = t;
}
}
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1); // strip BOM
cb(Papa.parse(text, { header: false, skipEmptyLines: true }));
};
reader.readAsArrayBuffer(file);
}
05The Builder / Builder 介面配置
The builder is a browser-only HTML interface structured for real-time visualization and customization:
Builder 構建器介面採用靈活的響應式前端排版,能極大地優化管理人員的錄入與校對體驗:
- 40:60 Side-by-Side Split / 40:60 雙欄分屏: The page is split into a 40% width left-hand column for settings/CSV uploads and a 60% width right-hand column for the preview map. 頁面分為左側欄(40% 寬度,放置數據輸入與配置項)和右側欄(60% 寬度,放置地圖預覽)。
-
Sticky Map Preview / 粘性預覽定位:
The right column is styled as
position: sticky, pinning the map and generate controls to the top of the viewport. As users scroll through inputs on the left, the map remains visible, providing immediate visual feedback. 右欄設置為position: sticky,將地圖固定在視窗頂部。用戶在左側滾動修改配置時,地圖預覽依然固定在屏幕側,提供實時的視覺反饋。 -
Easter Egg Customizer Panels / 隱藏式高級自訂面板:
To keep the layout clean for standard users, the customizer panels (Step 2.1 and Step 2.2) are hidden by default. Clicking five times in rapid succession anywhere on the **Step 2 card** reveals these sections:
為保持界面整潔,高級自訂面板(步驟 2.1 和 2.2)默認隱藏。在 **Step 2 卡片** 的任意空白處快速點擊 5 次即可解鎖以下兩個卡片:
- Step 2.1 (Colors & RAR bands) / 步驟 2.1(顏色與色彩頻譜): Configure pending/no-data colors and edit the range spectrum bands table. 設置全局待調查/無數據顏色,並可任意添加、修改或刪除無鼠百分比顏色區間。
- Step 2.2 (Department badges) / 步驟 2.2(部門徽章): Map department abbreviations to custom badge colors. 自訂相機 ID 前綴(如 DSD、ArchSD)與其狀態徽章的配色。
-
Floating Preview Legend / 地圖內懸浮圖例:
The legend is built as a Leaflet control (
.info-legend) in the bottom-right corner of the preview map. This control updates dynamically based on the current preview selection (Pool vs. Surveillance vs. Result RAR). 預覽地圖內置了與成品地圖完全一致的懸浮圖例(.info-legend),並會根據當前切換的預覽階段(抽樣池 vs. 待調查階段 vs. 結束階段)動態刷新項目。
06The Template / 引擎模板結構
The template is a district-agnostic Leaflet map that functions as a consumer of window.APP_CONFIG. It dynamically resolves color matches, status badge styles, and legends on load:
地圖模板是一個與分區無關的 Leaflet 地圖,完全讀取配置負載 window.APP_CONFIG。它在加載時會動態解析色彩、徽章與圖例樣式:
-
Dynamic Color Matcher / 動態顏色匹配器:
getRARColor(rar)iterates over the dynamic array inCFG.colors.bands. If empty, it falls back to the legacy 3-tier FEHD spectrum.getRARColor(rar)會遍歷配置中的CFG.colors.bands區間數組,如果配置中沒有定義,則自動回退到食環署傳統的三級 RAR 頻譜。 -
Custom Department Badge Colors / 部門徽章配色解析:
Popup badges style their backgrounds dynamically by performing a lookup on the
CFG.departmentsprefix mapping dictionary. 氣泡窗中的部門徽章背景顏色通過對CFG.departments進行字典查找來動態著色。 - Dynamic Legend Redrawing / 圖例動態生成: The map's legend draws its items dynamically from the config bands during base layer transitions. 地圖加載和切換階段時,圖例會根據配置中的頻譜段動態重繪。
- Mosquito Layer Chronological Aggregator / 誘蚊器歷史合併解析: Groups raw monthly CSV entries by location, sorting history tables descending, and coloring the marker dynamically based on the latest available survey round. Supports global defaults as well as customized positive/negative colors per individual survey area. 將誘蚊器的月度多輪調查記錄按位置進行合併,並按逆序(最新月份在前)進行歷史數據排序,且地圖圖標的顏色始終反映最新一輪的調查狀態。支持全局默認顏色設置,以及針對各個調查分區單獨自訂陽性/陰性代表顏色。
- Two-Pass Coordinate Propagation / 雙階段經緯度坐標補全: Coordinates are only required once per gravidtrap. The builder performs a first pass to index the last valid coordinates entered in the CSV, and a second pass to automatically propagate these coordinates to any empty rows matching the same survey area and gravidtrap number. 每個誘蚊器在歷史記錄中僅需提供一次經緯度坐標。Builder 會先進行第一輪掃描以記錄 CSV 中最後一次為該誘蚊器輸入的有效坐標,並在第二輪掃描中將該坐標自動補全到相同調查區域及編號的空白行中。
- Final Coordinate Selection / 最新坐標對齊機制: If a gravidtrap has different coordinates defined across different survey rounds, the map engine collapses them to a single marker and aligns the plotted pin location with the last (final) coordinates defined in the CSV. 如果某個誘蚊器在不同的調查週期中填寫了不同的坐標,地圖引擎在將歷史記錄合併為單個圖標時,會以 CSV 中最後(最新)填寫的坐標作為該圖標在地圖上的實際位置。
Leaflet Integration & Marker Stacking / Leaflet 與標記重疊處理
When displaying thousands of points and avoiding exact marker overlaps (e.g. food licence premises sharing the exact building coordinate), the template integrates Leaflet.markercluster with OverlappingMarkerSpiderfier (OMS) and applies a coordinate jitter:
為應對數千個數據點並處理完全重疊的相機/許可證坐標,模板結合了 Leaflet.markercluster 與 OverlappingMarkerSpiderfier (OMS),並對重疊坐標進行微幅抖動(Jitter):
// 1. Cluster Group Configuration (Handing spiderfying off to OMS)
function makeFoodCluster() {
return L.markerClusterGroup({
disableClusteringAtZoom: 17, // Uncluster at zoom >= 17 to expose individual markers
maxClusterRadius: 30, // Smaller radius for tighter clusters
spiderfyOnMaxZoom: false, // Disable default spiderfier; OMS takes over instead
showCoverageOnHover: false,
iconCreateFunction: function(cluster) {
const count = cluster.getChildCount();
const size = count < 10 ? 28 : count < 50 ? 34 : 40;
return L.divIcon({
html: `<div style="width:${size}px;height:${size}px;background:rgba(21,101,192,0.75);border:2px solid #1565c0;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:0.78rem;box-shadow:0 1px 4px rgba(0,0,0,0.3);">${count}</div>`,
className: '',
iconSize: [size, size],
iconAnchor: [size/2, size/2]
});
}
});
}
// 2. Coordinate Jittering Formula (Radiating duplicate coordinates slightly outward)
const JITTER_R = 0.000045; // ~5 metres at HK latitude
const key = lat.toFixed(6) + ',' + lng.toFixed(6);
const total = coordCount[key] || 1;
if (total > 1) {
const idx = coordIndex[key]++;
const angle = (2 * Math.PI * idx) / total;
lat += JITTER_R * Math.sin(angle);
lng += JITTER_R * Math.cos(angle);
}
// 3. Registering markers with OMS and adding to Cluster Group
const marker = L.marker([lat, lng], { icon });
oms.addMarker(marker);
foodLayer.addLayer(marker);
Map Tile Layers & Custom Leaflet Panes / 地圖瓦片圖層與 Leaflet 自訂窗格
To provide high-quality mapping options, the template supports multiple base map tile layers (CartoDB, unofficial Google Maps, and official Hong Kong Government Geodata API). It uses Leaflet's custom map panes (specifically hkLabels with zIndex: 250 and pointerEvents: 'none') to ensure English and Chinese street names and labels remain layered above all markers and lot boundaries, ensuring text is never covered:
為提供高質量的地圖背景,模板支援多種底圖瓦片圖層(CartoDB、非官方 Google Maps 以及地政總署官方地理空間數據 API)。它利用了 Leaflet 的自訂地圖窗格(自訂 hkLabels 窗格,設 zIndex: 250 與 pointerEvents: 'none'),確保英文與中文街道地名標籤永遠渲染在所有標記與地段邊界之上,使其不被覆蓋:
// 1. CartoDB Gray Base Map (Default)
const lightMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap © CARTO',
subdomains: 'abcd',
maxZoom: 20
});
// 2. Google Maps Tile Layers (Unofficial, using mt0-mt3 subdomains)
const googleStreets = L.tileLayer('http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', {
maxZoom: 20,
subdomains: ['mt0','mt1','mt2','mt3'],
attribution: '© Google Maps'
});
const googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
maxZoom: 20,
subdomains: ['mt0','mt1','mt2','mt3'],
attribution: '© Google Maps'
});
// 3. HK Government Geodata base map and separate English/Chinese labels
const landsDBase = L.tileLayer('https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/basemap/wgs84/{z}/{x}/{y}.png?key=' + HK_GOV_API_KEY, { maxZoom: 20 });
const landsDLabelEn = L.tileLayer('https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/en/wgs84/{z}/{x}/{y}.png?key=' + HK_GOV_API_KEY, { maxZoom: 20, pane: 'hkLabels' });
const landsDLabelTc = L.tileLayer('https://mapapi.geodata.gov.hk/gs/api/v1.0.0/xyz/label/hk/tc/wgs84/{z}/{x}/{y}.png?key=' + HK_GOV_API_KEY, { maxZoom: 20, pane: 'hkLabels' });
const hkGovGroupEn = L.layerGroup([landsDBase, landsDLabelEn]);
const hkGovGroupTc = L.layerGroup([landsDBase, landsDLabelTc]);
// 4. Creating a separate pane to float street labels above overlays
map.createPane('hkLabels');
map.getPane('hkLabels').style.zIndex = 250;
map.getPane('hkLabels').style.pointerEvents = 'none';
LandsD Private Land Lots Boundary Layers / 地政總署土地邊界圖層
To display private land parcels, government land allocations (GLA), and short-term tenancies (STT) without hitting API limits or causing browser lags, the template uses a sophisticated spatial tiling layer. It only renders when zoomed close enough (level 19+) and performs concurrent transformations on polygon vertices in batches while caching them:
為渲染私人地段、政府土地撥用(GLA)及短期租約(STT)邊界,同時避免超出 API 限制或導致瀏覽器卡頓,地圖模板採用了精細的空間瓦片圖層。此功能僅在縮放至 19 級或以上時啟用,並在客戶端進行多線程並發坐標轉換與頂點快取(Cache):
Coordinate System Mismatch & Vertex Transformation / 坐標系統差異與頂點轉換
The core challenge when integrating LandsD land boundary polygons is the grid coordinate mismatch:
- Map Coordinates (WGS84): Leaflet maps and GPS locators use decimal lat/lng degree coordinates (WGS84, EPSG:4326).
- Land Parcel Coordinates (HK1980 Grid): LandsD maps and indexes land boundaries using planar grid coordinates (Easting/Northing in metres, EPSG:2326). The returned GML XML geometries consist of coordinates in this format.
- Vertex Deduplication & Cache (
lotVertexCache): Adjacent land lots share boundary vertices. The template rounds vertex coordinates to 1 decimal place (10cm precision) and checks a persistent Map cache. This reduces API requests by up to 90%. - Pooled Concurrency (
lotPooled): Uncached unique vertices are transformed in parallel batches of 700 with a 50ms batch delay, preventing API rate-limit saturation.
在整合地政總署土地邊界多邊形時,核心挑戰在於坐標系統的差異:
- 地圖坐標系統 (WGS84): Leaflet 地圖和 GPS 定位使用的是十進制經緯度坐標(WGS84, EPSG:4326)。
- 土地邊界坐標系統 (HK1980 網格): 地政總署使用平面網格坐標(以米為單位的東距/北距,EPSG:2326)來繪製與索引土地邊界。返回的 GML XML 幾何圖形即由此坐標構成。
- 頂點去重與快取 (
lotVertexCache): 相鄰地段共享邊界頂點。模板將頂點坐標四捨五入至小數點後一位(10公分精度)並存入 Map 快取,減少了高達 90% 的 API 請求。 - 並發池控制 (
lotPooled): 將未快取的頂點以 700 個為一組進行分批並發轉換,批次間設 50 毫秒延遲,以防超出 API 流量頻寬。
// 1. Constants and endpoint definitions
const LOT_BASE = 'https://mapapi.geodata.gov.hk/gs/api/v1.0.0/iC1000';
const LOT_TYPES = ['lot', 'gla', 'stt'];
const LOT_MIN_ZOOM = 19; // Gate: active only at zoom 19+
const LOT_TILE_E = 700; // Tile width in HK80 metres (< 750m API limit)
const LOT_TILE_N = 550; // Tile height in HK80 metres (< 600m API limit)
const LOT_CONCURRENCY = 700; // Maximum parallel coordinate transformation calls
const lotVertexCache = new Map(); // Caches: "E|N" -> [wgsLat, wgsLng]
// 2. Dynamic spatial tiling of the viewport (in HK80 coordinates)
const bboxes = [];
for (let e = hkSW.E; e < hkNE.E; e += LOT_TILE_E) {
for (let n = hkSW.N; n < hkNE.N; n += LOT_TILE_N) {
bboxes.push([
e.toFixed(1),
n.toFixed(1),
Math.min(e + LOT_TILE_E, hkNE.E).toFixed(1),
Math.min(n + LOT_TILE_N, hkNE.N).toFixed(1)
]);
}
}
// 3. Request URL format (returns GML format)
// URL: `${LOT_BASE}/${type}?bbox=${minE},${minN},${maxE},${maxN},EPSG:2326`
// 4. Concurrent Pooled Batch Execution
const LOT_BATCH_DELAY = 50; // delay between batches in ms
async function lotPooled(items, asyncFn, batchSize) {
const results = new Array(items.length);
for (let start = 0; start < items.length; start += batchSize) {
const end = Math.min(start + batchSize, items.length);
const batch = items.slice(start, end);
const batchResults = await Promise.all(batch.map((item, j) => asyncFn(item, start + j)));
batchResults.forEach((r, j) => { results[start + j] = r; });
if (end < items.length) await new Promise(res => setTimeout(res, LOT_BATCH_DELAY));
}
return results;
}
// 5. DOM XML/GML coordinates parser
function parseLotGML(xmlText, litType) {
const doc = new DOMParser().parseFromString(xmlText, 'application/xml');
const out = [];
doc.querySelectorAll('featureMember').forEach(member => {
const props = {};
member.querySelectorAll('*').forEach(el => {
if (!el.children.length && el.textContent.trim()) props[el.localName] = el.textContent.trim();
});
const rings = [];
member.querySelectorAll('Polygon exterior LinearRing posList, Polygon interior LinearRing posList').forEach(node => {
const nums = node.textContent.trim().split(/\s+/).map(Number);
const ring = [];
for (let i = 0; i + 1 < nums.length; i += 2) {
if (!isNaN(nums[i]) && !isNaN(nums[i+1])) ring.push([nums[i], nums[i+1]]);
}
if (ring.length >= 3) rings.push(ring);
});
if (rings.length) out.push({ litType, props, rings });
});
return out;
}
07The Product / 地圖產品
The generated product file is a static, self-contained single-page application. It inherits the custom color schemes, dynamic legend tiers, and custom status badge designs created in the builder, providing a consistent user experience matching the builder's preview maps. It loads Leaflet assets via CDN, allowing it to run locally on desktop browsers without dependencies.
生成的產品文件是一個獨立的單頁網頁。它繼承了在 Builder 中配置的自訂配色方案、動態圖例層級和自訂狀態徽章設計,提供與 Builder 預覽地圖相匹配的一致用戶體驗。地圖通過 CDN 加載 Leaflet 相關庫,可在桌面端瀏覽器直接打開運行。
08Technical specifications / 技術規格
Libraries (CDN-loaded): Leaflet 1.9.4, PapaParse 5.4.1, Leaflet.markercluster, OverlappingMarkerSpiderfier (OMS).
第三方 CDN 庫: Leaflet 1.9.4、PapaParse 5.4.1、Leaflet.markercluster、OverlappingMarkerSpiderfier (OMS)。
Dynamic CSS Classes in Builder / CSS 樣式命名:
.info-legend: Style rules for the Leaflet-inlined preview map map legend card. 預覽地圖中懸浮圖例的卡片樣式。.legend-item&.legend-color: List elements displaying the color circle and label inline. 圖例項的色圈與文本對齊樣式。.cust-table,.cust-add&.row-del: Styles for customization tables and buttons. 自訂範圍/部門編輯表格與按鈕樣式。
Department Regex Generator: The builder dynamically matches prefix codes (e.g. HD, DSD) from camera IDs using a generated regex checker: /(^|_)PREFIX(_|$)/i, with FEHD acting as the default fallback.
部門正則匹配器: Builder 根據自訂字典,為每個部門前綴動態組裝正則表達式 /(^|_)PREFIX(_|$)/i 來解析 CSV 中的相機 ID,匹配失敗則回退為 FEHD。
Department ID Prefix Parser / 部門前綴解析器
The builder parses camera IDs to isolate department abbreviations and serial numbers using custom badges configuration:
構建器根據自訂徽章設置解析相機 ID,以分離出部門縮寫及序號:
function parseId(raw) {
const id = (raw || "").trim();
const up = id.toUpperCase();
let dept = "FEHD"; // default fallback
const keys = Object.keys(customDepts);
for (let k of keys) {
if (k === "FEHD") continue;
const regex = new RegExp("(^|_)" + k + "(_|$)", "i");
if (regex.test(up)) {
dept = k;
break;
}
}
const m = id.match(/(\d+)\s*$/);
const serial = m ? String(parseInt(m[1], 10)) : id;
return { dept, serial };
}
CSDI Public APIs & Geodetic Integration / CSDI 公共 API 與地理空間整合
To enable location search, address querying, and lamp post mapping, the template integrates several government spatial services hosted on geodata.gov.hk and portal.csdi.gov.hk. To support high-density features (e.g. >10,000 lamp posts per district), the template implements dynamic viewport bounding-box envelope queries to prevent browser rendering lag. Coordinates transformation is handled client-side:
為實現地點搜尋、地址查詢以及燈柱定位,地圖模板整合了多個由政府地理空間數據門戶網站(geodata.gov.hk 及 portal.csdi.gov.hk)託管的服務。針對燈柱等超高密度的地理特徵,系統採用了視窗包圍盒(Viewport Bounding Box)動態分頁加載以防瀏覽器卡頓,並在客戶端進行坐標系統轉換:
// 1. Location Search API (Lands Department / CSDI)
const LOCATION_SEARCH_API = 'https://www.map.gov.hk/gs/api/v1.0.0/locationSearch';
// URL Format: https://www.map.gov.hk/gs/api/v1.0.0/locationSearch?q=[query]
// Returns flat JSON array with fields: nameEN, nameZH, addressEN, addressZH, districtEN, districtZH, x, y (HK80 grid)
// 2. Coordinates Transformation API (Lands Department / Survey & Mapping Office)
const COORD_TRANSFORM_API = 'https://www.geodetic.gov.hk/transform/v2/';
// Converts HK80 Grid (x/Easting, y/Northing) → WGS84 (lat, lng)
async function hk80ToWgs84(easting, northing) {
const url = `${COORD_TRANSFORM_API}?inSys=hkgrid&outSys=wgsgeog&e=${easting}&n=${northing}`;
const res = await fetch(url);
if (!res.ok) throw new Error('Coord transform HTTP ' + res.status);
const data = await res.json();
if (data.ErrorCode) throw new Error('Coord transform error ' + data.ErrorCode);
return [data.wgsLat, data.wgsLong]; // returns [lat, lng]
}
// 3. CSDI Viewport-Based Lamp Post Query (Services ID: hyd_rcd_1629267205229_84645)
const LAMPPOST_SERVICE_ID = 'hyd_rcd_1629267205229_84645';
// Queries CSDI ArcGIS feature server using bounding box envelope and district name (corrected field name: LAMP_NO)
// URL Query Params: geometry=west,south,east,north&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&inSR=4326
// SQL Where filter: `DISTRICT = '[District]'`
// Real-time autocomplete debounced by 250ms: `DISTRICT = '[District]' AND LAMP_NO LIKE '[Input]%'`
// 4. CSDI Food Licence API Service IDs
// Restaurant Licences Typename/ID: FEHD_RL (fehd_rcd_1630036390312_58893)
// Other Food Licences Typename/ID: FEHD_FL (fehd_rcd_1630036111498_75446)
// Pre-filtering district queries by district code: "NSEARCH01_EN = '[DISTRICT_CODE]'"
CSDI WFS Resilience & CORS Fallback / 數據彈性獲取機制
To ensure food licence data loads even when direct CORS access is blocked or public proxies time out, the template uses a timed fallback loop across several proxies:
為確保即使直連受限或公共代理超時,仍能成功加載食物牌照 GeoJSON 數據,模板實施了多代理輪詢與超時機制:
const CORS_PROXIES = [
u => 'https://api.allorigins.win/raw?url=' + encodeURIComponent(u),
u => 'https://corsproxy.io/?url=' + encodeURIComponent(u),
u => 'https://api.codetabs.com/v1/proxy?quest=' + encodeURIComponent(u)
];
const PROXY_ROUNDS = 2;
const FETCH_TIMEOUT_MS = 20000;
async function fetchWithTimeout(url, ms, opts) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try { return await fetch(url, Object.assign({ signal: ctrl.signal }, opts || {})); }
finally { clearTimeout(t); }
}
async function fetchJsonResilient(url) {
try {
const res = await fetchWithTimeout(url, FETCH_TIMEOUT_MS, { mode: 'cors' });
if (res.ok) return await res.json();
} catch (e) {
console.warn('Direct fetch failed, falling back to proxies...');
}
for (let round = 1; round <= PROXY_ROUNDS; round++) {
for (const wrap of CORS_PROXIES) {
try {
const res = await fetchWithTimeout(wrap(url), FETCH_TIMEOUT_MS);
if (res.ok) return await res.json();
} catch (e) {
console.warn('Proxy failed, trying next...');
}
}
}
throw new Error('All fetch attempts failed.');
}
CSDI Public APIs & Geodetic Integration / CSDI 公共 API 與地理空間整合
To enable location search, address querying, and lamp post mapping, the template integrates several government spatial services hosted on geodata.gov.hk and portal.csdi.gov.hk. Since some APIs return coordinates in the local Hong Kong 1980 Grid system (Easting/Northing), they are transformed client-side into WGS84 coordinates using the official Geodetic Coordinates Transformation API:
為實現地點搜尋、地址查詢以及燈柱定位,地圖模板整合了多個由政府地理空間數據門戶網站(geodata.gov.hk 及 portal.csdi.gov.hk)託管的服務。由於部分 API 返回的是香港 1980 地理網格坐標(東距/北距),系統會在客戶端調用大地測量坐標轉換 API 將其即時轉換為 WGS84 經緯度坐標:
// 1. Location Search API (Lands Department / CSDI)
const LOCATION_SEARCH_API = 'https://www.map.gov.hk/gs/api/v1.0.0/locationSearch';
// URL Format: https://www.map.gov.hk/gs/api/v1.0.0/locationSearch?q=[query]
// Returns flat JSON array with fields: nameEN, nameZH, addressEN, addressZH, districtEN, districtZH, x, y (HK80 grid)
// 2. Coordinates Transformation API (Lands Department / Survey & Mapping Office)
const COORD_TRANSFORM_API = 'https://www.geodetic.gov.hk/transform/v2/';
// Converts HK80 Grid (x/Easting, y/Northing) → WGS84 (lat, lng)
async function hk80ToWgs84(easting, northing) {
const url = `${COORD_TRANSFORM_API}?inSys=hkgrid&outSys=wgsgeog&e=${easting}&n=${northing}`;
const res = await fetch(url);
if (!res.ok) throw new Error('Coord transform HTTP ' + res.status);
const data = await res.json();
if (data.ErrorCode) throw new Error('Coord transform error ' + data.ErrorCode);
return [data.wgsLat, data.wgsLong]; // returns [lat, lng]
}
// 3. CSDI Lamp Post Query API (Services ID: hyd_rcd_1629267205229_84645)
const LAMPPOST_SERVICE_ID = 'hyd_rcd_1629267205229_84645';
// Detects lamp post query via: /^[A-Za-z]{1,3}\d{2,6}[A-Za-z]?$/
// SQL Where filter: `Lamp_Post_Number = '[NO]'`
const where = `Lamp_Post_Number = '${lampNo.replace(/'/g, "''")}'`;
// 4. CSDI Food Licence API Service IDs
// Restaurant Licences Typename/ID: FEHD_RL (fehd_rcd_1630036390312_58893)
// Other Food Licences Typename/ID: FEHD_FL (fehd_rcd_1630036111498_75446)
// Pre-filtering district queries by district code: "NSEARCH01_EN = '[DISTRICT_CODE]'"
09Key design decisions / 關鍵設計決策
- Double-Column Layout with Sticky Preview / 雙欄配置與粘性定位: Eliminates scrolling fatigue. Users get immediate feedback in the viewport as they adjust ranges, colors, and CSV inputs. 將配置輸入與預覽分欄展示,地圖隨網頁滾動粘性固定,讓用戶在調整參數或上傳 CSV 時,可以在右側視窗中獲得最直觀的即時視覺反饋。
- Easter Egg Customizer Panels / 隱藏式自訂面板設計: Balances advanced power with clean design. Less-technical users are not distracted by custom spectrum details, while advanced users can toggle them via the 5-click easter egg. 平衡了界面簡潔度與高級自訂功能。普通操作人員不會被複雜的色彩頻譜設置分心,而需要高級調整的用戶可以通過點擊 5 次輕鬆解鎖。
- Preserving CSV Data in Memory / 內存保留原始數據: Keeping the uploaded CSV data in memory allows the builder to instantly re-parse coordinates and map prefixes if the user modifies department badge prefix rules, without requiring the user to re-upload files. 將原始 CSV 保存在瀏覽器會話中。用戶在修改部門徽章顏色或新增前綴時,Builder 能即時重新解析地圖前綴,免去了繁瑣的重複上傳步驟。
- In-Map Floating Legends / 地圖內懸浮圖例: By placing the legend inside Leaflet's control container rather than a static list below the map, the preview map is identical in presentation to the generated product. 將圖例內置於 Leaflet 地圖的右下角,而非像以前一樣堆疊在下方,保證了預覽地圖與最終導出的網頁在呈現效果上完全相同。
- Viewport-Based Lamp Post Query & Zoom Gate / 視窗邊界動態加載與縮放限制: To support districts with >10,000 lamp posts, rendering is constrained to zoom ≥ 17. The map queries CSDI only for features within the current bounding box envelope, replacing clustered pins with tiny 6px dots and showing labels only at zoom ≥ 18 to optimize memory and maintain layout clarity. 針對擁有超過一萬根燈柱的行政區,系統限制在縮放層級大於或等於 17 時才載入數據。地圖僅針對目前網頁視窗邊界(Bounding Box)向 CSDI 發送包圍盒查詢,並將標記優化為 6px 微型圓點,僅在縮放 ≥ 18 時顯示編號,大大節省了內存佔用並保持畫面整潔。
10Running & viewing / 運行與部署
Local Server: The builder and template must be served from an http(s) origin (e.g. `python -m http.server`) for the generate step to fetch the dynamic template file. The generated `index.html` runs by double-click anywhere.
本地服務器需求: 由於「生成」步驟涉及異步獲取(Fetch)地圖模板文件,Builder 必須在 HTTP(S) 協議環境下打開(例如使用編輯器的 Live Server,或本地終端運行 python -m http.server)。生成並下載後的 index.html 則可以隨意在本地雙擊打開運行。
Template Assembly & Unicode Escaping / 模板編譯與 Unicode 轉義
The builder compiles the active parameters, escapes Chinese characters to ASCII-safe code points to prevent file distribution corruption, and replaces the target comment in the template:
構建器會編譯自訂參數,將中文字符轉換為 ASCII 安全的轉義序列以防亂碼,並替換模板中的註釋標籤:
// 1. Compile configurations and escape Unicode characters to \uXXXX
const json = JSON.stringify(cfg).replace(/[\u0080-\uffff]/g,
c => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
);
const inject = "window.APP_CONFIG = " + json + ";";
// 2. Perform safe replacement of the template comment placeholder
html = html.replace("/*__APP_CONFIG__*/", () => inject);
// 3. Trigger standard browser download
function download(filename, text, mimeType) {
const blob = new Blob([text], { type: mimeType });
const a = document.createElement("a");
a.download = filename;
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href);
}
11Other considerations / 其他注意事項
- Performance / 渲染性能: The dynamic legend loop is highly optimized. Redrawing happens sequentially, allowing the browser to handle thousands of custom markers without stuttering. 動態圖例與渲染邏輯均經過優化,切換底圖或過濾無鼠百分比時非常流暢,可輕鬆應對數千個相機標記。
- Data Integrity / 數據完整性: Range inputs are validated to ensure min and max values remain within logical bounds (0 to 100). 色彩區間輸入具備防錯驗證,確保數值下限與上限不超出合理界限(0 至 100 之間)。
12Rebuild checklist / 重構檢查清單
If rebuilding this customizable system from scratch:
如果未來的開發人員需要從頭重建這個高度自訂化的系統,請遵循以下步驟:
-
Define the Configuration Contract First / 首先定義數據合約:
Ensure
APP_CONFIGcontains nested structures for custom global colors, range bands, and department badge colors. 設計配置架構,確保合約包含自訂色彩、色彩頻譜數組以及部門徽章對照字典。 - Build a 2-Column Responsive UI / 實現雙欄響應式佈局: Split inputs and preview maps into columns. Apply position: sticky to the preview map so it stays visible while scrolling. 分割輸入欄與地圖預覽,對預覽側進行 sticky 粘性定位以在滾動時保持可見。
- Implement an Encoding Ladder & Prefix Parser / 實現編碼檢測與動態前綴解析: Keep raw CSV data in memory. Read files as raw bytes, try UTF-8/Big5/GB18030, and extract prefixes dynamically using prefix dictionaries. 保留 CSV 在內存中。利用編碼梯消除 Traditional Chinese 亂碼,並基於自訂字典正則表達式動態提取部門前綴。
- Enable Dynamic Legend Redrawing / 啟用 Leaflet 動態圖例: Implement dynamic controls (`L.Control`) in Leaflet to draw the legend based on the config. 在 Leaflet 中創建控制件以讀取動態色彩段並繪製地圖內懸浮圖例。
- Protect Standard Configurations / 保護標準預設參數: Standard department tags and base parameters should have built-in read-only fallback values. 為常用部門(FEHD、LCSD、HD)提供只讀保護,避免用戶誤刪除基礎設置。
13Glossary / 術語表
| Term / 術語 | Description / 說明 |
|---|---|
| RAR Color Bands 無鼠百分比色彩段 |
Range thresholds (min to max) paired with hex colors to color map markers by rodent absence rates. 一組無鼠百分比區間上限、下限以及對應的十六進制顏色,用於控制地圖相機標記的著色。 |
| Department Badges 部門Badge徽章 |
Status tags showing ownership (FEHD, LCSD, HD, custom depts) in marker details, colored according to configured badge rules. 地圖氣泡窗中顯示相機所屬部門(如食環署、康文署、房署或自訂部門)的標籤,顏色由自訂配置決定。 |
| APP_CONFIG 地圖合約配置 |
The single configuration object that fully describes one district's map. 完整描述一個行政分區地圖所需的單個 JSON 配置對象,充當 Builder 與模板之間的唯一接口。 |
| Gravidtraps (Mosquito Surveillance) 誘蚊器指數(蚊媒監測) |
Surveillance traps deployed by the FEHD across administrative districts to monitor dengue vectors, displaying live index status on the map. 由食環署部署於各行政分區的監測誘蚊器,用於登革熱傳播媒介監測,並在地圖上呈現最新一輪的陽性/陰性指數狀態。 |