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: 一個完全自訂化、由數據驅動的地圖平台,用於選定行政分區、自訂色彩、配置無鼠百分比區間,並生成最終部署的地圖網頁。

本開發設計手冊詳細說明了鼠隻活動調查(RAS)地圖構建系統的整體架構、數據流向、配置合約與樣式規則,旨在為開發人員、終端用戶及未來的系統維護者提供一份完整的、獨立的參考文檔。

00User Guide (Bilingual) / 使用手冊(雙語)

This user guide is designed for pest control officers and administrative staff to easily configure and deploy district rodent/mosquito surveillance maps using the RAS Map Builder, and navigate the resulting map.

本使用手冊專為防治蟲鼠人員及行政管理人員設計,旨在引導用戶如何使用 RAS 地圖構建器 來配置並生成分區防鼠/防蚊部署地圖,以及如何操作和分享生成的地圖網頁。

1. Operational Workflow / 工作流程概述

The builder compiles all inputs (configurations, customized colors, and uploaded CSV records) directly into a single, self-contained HTML page (index.html). This file functions independently without a database, and can be shared via email or uploaded to any web server.

地圖構建器會將您輸入的所有配置、自訂顏色以及上傳的 CSV 數據直接編譯進一個單一的、獨立的 HTML 網頁文件(index.html)。該文件可在無數據庫環境下獨立運行,並可直接通過電郵分發或上傳至伺服器發布。

2. Operating the Builder / 構建器(builder.html)操作指引

  • Step 1: District Settings / 步驟 1:行政分區與地圖標題設定 Select the administrative district (e.g. Tai Po). This selection triggers three automatic updates:
    • Map Viewport Setup: Centers the Leaflet preview on the district's primary coordinates and sets the initial zoom level.
    • CSDI API & Data Integrations: Connects to the district-specific datasets. It maps the correct district parameters to query and filter CSDI Lamp Posts, CSDI Food Premises/Licences, and the official CSDI Administrative Boundaries, restricting all spatial queries within that district's envelope to optimize network and memory.
    • Bilingual Labels: Populates the default English and Chinese titles shown in the compiled header.
    選擇行政分區(如 大埔區)。此選擇會觸發三個自動關聯更新:
    • 地圖預覽視窗設定: 自動將 Leaflet 預覽地圖中心定位於該區的核心經緯度,並配置預設的縮放層級。
    • CSDI 數據接口與整合關聯: 連接與該行政區相關的數據集。系統會自動映射正確的分區參數,用以篩選和查詢 CSDI 公共燈柱CSDI 食物業牌照/處所 以及官方的 CSDI 行政區界線,從而將所有空間查詢限制在該區範圍內以優化網絡與內存性能。
    • 雙語地圖標題: 自動生成將顯示在最終地圖頂部的默認中英文標題。
  • Step 2: Survey Phases / 步驟 2:監測階段與相機數據上傳 Upload camera survey datasets (one CSV per Phase).
    • Camera ID & Department Badge Correlation: The prefix of the Camera ID (e.g., the TP in TP_038 or HD in HD_TP_606) is correlated with the department colors defined in Step 2.2: Department Badges. If a camera starts with HD, it automatically adopts the Housing Department badge color on rendering.
    • RAR (%) and Marker Status Correlation:
      • If the RAR (%) is populated with a number (0-100), the camera is rendered as an Active circle marker, with its fill color determined by the customized spectrum bands in Step 2.1: Colors & RAR color bands.
      • If the RAR (%) is left blank, it indicates that the survey round is still Pending / In-Progress. The marker is automatically colored olive green, and clicking the camera will display its status as "Pending / 待調查" in the popup.
    上傳相機調查數據集(每個調查階段上傳一個 CSV 文件)。
    • 相機 ID 與部門徽章色彩的關聯: Camera ID 的前綴(例如 TP_038 中的 TPHD_TP_606 中的 HD)與步驟 2.2:部門標籤中定義的部門顏色相關聯。如果相機 ID 以 HD 開頭,它在渲染時會自動採用房屋署的徽章顏色。
    • 無鼠百分比(RAR %)與標記狀態的關聯:
      • 如果 RAR (%) 填寫了數值(0-100),相機將渲染為已完成的圓形標記,其填充顏色由步驟 2.1:色彩頻譜與無鼠率區間中自訂的頻譜色段決定。
      • 如果 RAR (%) 留空,代表該選點的調查仍在進行中(待調查)。標記將自動著色為橄欖綠色,點擊相機圖標時,彈出窗會顯示狀態為「Pending / 待調查」
  • Step 2.1: Colors & RAR Color Bands / 步驟 2.1:色彩頻譜與無鼠率區間(隱藏自訂面板) Set the color mappings for pending surveillance and missing data, and design the color spectrum bands for camera RAR percentages. These bands are directly correlated with:
    • The fill color of the Active camera markers.
    • The dynamic building of the Leaflet Map Legend.
    設定待調查與無數據時的配色,並為相機 RAR 百分比自訂色彩頻譜段。這些區間與以下內容直接關聯:
    • 已調查相機標記點的填充顏色。
    • 地圖右下角 Leaflet 圖例項目的動態生成與重繪。
  • Step 2.2: Department Badges / 步驟 2.2:部門標籤與徽章色彩(隱藏自訂面板) Map department abbreviations (e.g. FEHD, LCSD, HD) found in camera IDs to their custom status badge colors on the map. Changing these values instantly updates the badge styles in both the preview and the final map. 將相機 ID 前綴中的部門簡寫代號(如 FEHDLCSDHD)映射到特定徽章顏色。修改這些值會使預覽地圖和生成地圖中的部門徽章背景顏色同步更新。
  • Step 2.3: Gravidtraps / 步驟 2.3:分區監測與誘蚊器數據上傳(隱藏自訂面板) Upload the gravidtrap surveillance dataset.
    • Two-Pass Coordinate Propagation: If a gravidtrap is surveyed over multiple rounds (e.g., April, May, and June), you only need to enter its Latitude and Longitude **once** in any of its rows. The parser indexes coordinates in the first pass and automatically propagates them to any empty rows in the second pass.
    • Last Entry Coordinates Priority: If a gravidtrap has different coordinates entered in different rows (e.g., due to relocation or adjustment), the map engine will always adopt the **last/final coordinate** in the CSV to plot the physical marker, ensuring the pin represents the most up-to-date physical position.
    • Survey Area Color Overrides: By default, positive gravidtraps render in red and negative in yellow. Under Step 2.3, you can override these defaults for **individual survey areas** (e.g., coloring Tai Po East as pink/green and Tai Po West as purple/cyan) to highlight sub-zone classifications.
    💡 Tip (Coordinate Propagation) / 💡 經緯度自動補全提示:
    Coordinates are only required once per physical gravidtrap. If a gravidtrap is surveyed in multiple rounds (different rows/months), you can leave its latitude and longitude blank in subsequent rows. The builder will automatically copy the coordinates from the row where they were first defined. 每個誘蚊器在歷史記錄中僅需填寫一次經緯度。如果該編號的誘蚊器在多個月份進行了多輪調查(多行數據),後續行的經緯度可以留空,系統會自動在後台將之前填寫過的坐標補全過來。
    上傳分區誘蚊器調查數據集。
    • 雙階段經緯度補全機制:如果某個誘蚊器在歷史記錄的多個週期(例如4月、5月和6月)中被重複調查,您只需在 CSV 的其中一行中填寫其 Latitude 和 Longitude。系統會在第一輪掃描中收集坐標,並在第二輪掃描中自動補全到其他經緯度空白的行中。
    • 最新坐標對齊原則:當同一個監測點在 CSV 的不同週期行中被填寫了不同的經緯度(例如因位置調整),地圖引擎在渲染時會始終以 CSV 中最後錄入(最下方)的坐標作為該圖標的物理定位,確保地圖顯示最新位置。
    • 調查分區色彩覆蓋:預設情況下,呈陽性的監測點顯示為紅色,呈陰性的顯示為黃色。您可以在「步驟 2.3」中為個別調查分區設置自訂的陽性/陰性代表顏色(例如將大埔東區設置為粉紅/綠色,大埔西區設置為紫色/青色),以便突出子區域的分類。
  • Step 3: Sampling Pools / 步驟 3:相機抽樣池上傳(選填) Upload the district's camera pool CSV. The builder automatically matches the Latitude and Longitude headers to plot the locations.
    💡 Dynamic Header-Driven Columns / 💡 表頭驅動動態欄位屬性:
    Except for the Latitude and Longitude columns (which are parsed by name to plot the markers), **every other column in the CSV is dynamic**. If you add new columns to the CSV (e.g. Contact Person, Phone Number, Last Inspection Date), the template will automatically detect them and render them as rows in the map popup bubbles on load. No code changes are required! 除了用於地圖標記定位的 LatitudeLongitude 經緯度列外,CSV 中的所有其他欄位都是動態的。如果您在 CSV 中增加了新列(如 聯絡人電話上次巡查日期),地圖模板會自動解析並將這些屬性以表格行的形式呈現在地圖氣泡彈出窗中,無需修改任何代碼!
    上傳該行政區的相機抽樣池 CSV。系統會自動定位 LatitudeLongitude 列表頭進行坐標標記。
  • Step 4: Compiling & Saving / 步驟 4:地圖編譯與輸出 Click the Generate map HTML button. The compiler verifies all datasets and performs a safe injection.
    💡 Phase-Optional Decoupled Compilation / 💡 零相機純蚊患地圖編譯:
    The builder does not require camera phases. You can compile a fully functional map with **0 camera phases** and only a gravidtrap CSV. The map engine template automatically detects this, hides all camera phase buttons and locator boxes, and displays a clean, dedicated mosquito surveillance dashboard. 構建器不需要您必須上傳相機數據。您可以只上傳一份誘蚊器 CSV(相機監測階段數為 0)來編譯地圖。地圖模板會自動識別該情況,隱藏所有相機控制按鈕與定位搜索欄,直接呈現出一個整潔的專用防蚊部署地圖。
    點擊 Generate map HTML 按鈕。系統會驗證所有數據,若無嚴重錯誤(如經緯度超出香港範圍或缺失必要欄位),瀏覽器會自動編譯並下載獨立的 index.html 地圖網頁到您的電腦中。

3. Using the Generated Map / 地圖網頁(index.html)操作指引

  • Layer Controls / 圖層切換與顯示 Use the circular filter buttons on the top header to toggle specific layers: Cameras (by Phase), Pools (camera pool database), and Gravidtraps (mosquito surveillance). 點擊頂部導航欄的圓形圖層按鈕,可以自由切換顯示:各階段的熱能相機、相機抽樣池、以及誘蚊器指數(誘蚊器圖層)。
  • Filtering the Gravidtrap Layer / 篩選與分析誘蚊器指數 When the Gravidtrap layer is active, an options drawer will expand:
    • Survey Area: Display only gravidtraps belonging to a specific sub-area (e.g. Tai Po East).
    • Round: Filter to a specific survey round (Year and Month combination). By default, the map displays the latest, most recent survey round of each gravidtrap.
    • Positive (+ve) Only: Filters to show only gravidtraps that are positive in the currently active round.
    • Once positive in the past: A powerful hotspot tracking tool. If checked, the map will display any gravidtrap that has had at least one positive round in its entire timeline, even if its currently selected round status is negative.
    當開啟誘蚊器圖層時,下方會展開篩選抽屜:
    • Survey Area (調查分區): 僅顯示屬於特定分區(如 大埔東)的誘蚊器。
    • Round (調查週期): 篩選特定年份與月份的調查數據。預設地圖會顯示每個誘蚊器的最新一期調查狀態
    • Positive (+ve) Only (僅顯示陽性): 僅篩選在當前選中週期裡呈現陽性(No. of GI Adult > 0)的誘蚊器。
    • Once positive in the past (歷史曾呈陽性): 熱點追蹤工具。選中時,地圖將篩選出在其歷史任何一期調查中曾出現過陽性記錄的誘蚊器,即使它在當前週期已轉為陰性,也會繼續在地圖上呈現,以便跟進。
  • Pin Interactivity & popups / 圖標互動與詳情彈窗 Click any marker to open its detail popup. Cameras display installation details, risk levels, and their exact RAR. Gravidtraps display their location description, management authority, and a full chronological timeline table of their historical survey rounds (with the latest round listed at the top). 點擊地圖上的任何圖標可打開詳情氣泡窗。相機圖標會展示安裝細節、風險等級及無鼠百分比。誘蚊器圖標則會展示其具體位置描述、管理單位以及一個按時間倒序排列的歷史調查週期清單表格(最新週期置於最頂部)。
  • Shareable Stateful URLs / 一鍵複製並分享篩選狀態 Any toggle or filter selection you make (e.g., active layers, zoom level, selected area, or checked filters) is automatically serialized into the browser's address bar URL hash (e.g. #mos=1&mosArea=Tai+Po+East&mosOnce=1). Simply copy the link from your browser's address bar and share it. When they open it, their map will load in the exact same view and state!
    💡 Share viewport and filter states instantly / 💡 一鍵分享特定視圖與篩選狀態:
    Instead of taking screenshots or typing instructions for colleagues, you can simply **copy the URL link from your address bar and share it**. When a colleague opens the link, the map will parse the hash values and automatically restore the exact layer visibilities, coordinates, zoom level, and drop-down filters! 無需截圖或向同事發送繁瑣的篩選文字說明,您只需直接複製瀏覽器地址欄中的網址並分享給同事。當他們打開網址時,地圖引擎會自動解析網址末尾的哈希參數,將圖層顯示、中心位置、縮放層級以及所有篩選下拉框還原為與您完全一致的狀態!
    您在地圖上進行的所有篩選操作(如切換圖層、縮放位置、選擇調查分區或勾選歷史陽性)都會即時序列化並反映在瀏覽器的地址欄 URL 哈希中(如 #lat=22.4501&lng=114.1681&zoom=15&mos=1&mosArea=Tai+Po+East&mosOnce=1)。您只需複製瀏覽器地址欄中的網址並分享給同事。當他們打開網址時,地圖網頁會自動加載並還原為與您完全一致的視圖和篩選狀態!

4. Frequently Asked Questions / 常見問題與故障排除

  • Why are some Chinese characters garbled? / 為什麼上傳的 CSV 中文字符會變成亂碼? This happens when Microsoft Excel saves the file in regional encoding (Big5/ANSI) instead of Unicode (UTF-8). Although the builder has built-in auto-detection for Big5, it is recommended to open the file in Excel, select "Save As", and choose CSV UTF-8 (Comma delimited) (*.csv) before uploading. 這通常是由於 Excel 以繁體中文編碼格式(Big5/ANSI)儲存 CSV 文件造成的。儘管構建器內置了自動亂碼校正機制,但最佳實踐是:在 Excel 中選擇「另存新檔」,並在存檔類型中選擇 CSV UTF-8 (逗號分隔) (*.csv),然後重新上傳。
  • Why was my record skipped with a coordinate boundary warning? / 為什麼提示記錄被跳過並出現「outside Hong Kong」警告? To prevent rendering errors, the builder filters out points with invalid GPS coordinates. Ensure that your coordinates are written in standard WGS84 decimal format (e.g., Latitude ~22.45, Longitude ~114.16) and that you haven't swapped the Latitude and Longitude columns. 為防止繪圖錯誤,系統會自動跳過經緯度不正確的數據。請檢查您的經緯度是否填寫為標準的 WGS84 十進制格式(例如緯度 22.45,經度 114.16),並確認沒有將緯度(Latitude)和經度(Longitude)兩列的內容倒置。
  • If multiple coordinates are entered for a single gravidtrap, which one is used? / 如果同一個誘蚊器填寫了多個不同的經緯度坐標,系統會使用哪一個? The map engine always aligns with the last (final) coordinate entered in the CSV for that gravidtrap. 地圖引擎會始終以 CSV 記錄中最後(最新)填寫的經緯度坐標作為該個誘蚊器在地圖上的最終標記位置。

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). To prevent configuration parameters from scattering, all customized styling and coordinates are bundled directly within the config payload. These roles map onto three distinct files in the root deployment directory:

系統在「構建時配置」與「運行時客戶端渲染」之間保持了清晰的分離。為防止配置參數分散,所有自訂樣式與相機坐標都直接捆綁在配置負載中。項目文件均放置於系統部署的根目錄中:

index.html (inside gemini_v2/) — Builder / 構建器

A self-contained tool with a 2-column input/preview layout, real-time map preview synchronization, and advanced color spectrum/department badge editors. Opens locally to compile and download maps. (Note: ras_map_builder.html is an obsolete single-phase builder that does not support gravidtrap data).

一個獨立的構建工具,配備雙欄輸入/預覽佈局、實時地圖預覽同步,以及先進的色彩頻譜與部門徽章編輯器。在本地打開以進行地圖的編譯和下載。(註:舊版 ras_map_builder.html 已廢棄,不支持蚊媒監測等新特徵)。

/map_template.html — Template / 地圖模板

The dynamic map application template, refactored to dynamically generate legends, marker colors, and popup badges based on parameters inside the injected config.

核心地圖應用模板,可根據注入的配置參數動態生成圖例、標記顏色及彈出窗徽章。

index.html (exported) — Product / 地圖產品

The finished, district-specific map, compiled and downloaded from the builder. Saved in a separate location or folder (e.g., test/index.html in the sandbox) to avoid overwriting the builder. Opened (view-only) by end-users.

由 Builder 生成的、特定分區的獨立地圖文件。應儲存於獨立目錄或分流文件夾中(例如本地測試沙盒中的 test/index.html),以防覆蓋 Builder 自身的 index.html 網頁,供終端用戶查看。

CSV files / 數據檔 camera & pool data District / 行政分區 dropdown & centre Customizer / 自訂規則 pending/no-data colors RAR bands & depts Builder validate · preview 2-column side-by-side assemble config Template /*__APP_CONFIG__*/ index.html customized map fetch inject config

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"
  }
};

19 Administrative Districts Mapping / 19 行政分區代碼對照表

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. Note: While there are 18 administrative districts in Hong Kong, Yau Tsim Mong is split into "Yau Tsim" (61) and "Mong Kok" (62) to align with FEHD food-licence and CSDI database conventions, resulting in 19 mapped entries:

構建器建立了香港行政分區與 CSDI/食環署食物牌照分區代碼(對應政府數據集中的 NSEARCH01_EN 欄位值)之對照字典,並設定了默認市中心坐標及地圖縮放層級。註:雖然香港設有 18 個行政區,但為配合食環署食物牌照分區及 CSDI 數據庫的劃分習慣,油尖旺區被拆分為「油尖」(61)和「旺角」(62),故共列出 19 個映射項目:

District / 分區 CSDI District Code / 分區代碼 Default Lat/Lng / 預設經緯度 Zoom / 縮放
Eastern / 東區1122.2829, 114.223014
Wan Chai / 灣仔區1222.2779, 114.173115
Southern / 南區1522.2470, 114.159014
Islands / 離島區1722.2090, 113.943012
Central/Western / 中西區1822.2870, 114.155015
Kwun Tong / 觀塘區5122.3130, 114.226015
Kowloon City / 九龍城區5222.3280, 114.191015
Wong Tai Sin / 黃大仙區5322.3420, 114.193615
Yau Tsim / 油尖區6122.3050, 114.169015
Mong Kok / 旺角區6222.3190, 114.169015
Sham Shui Po / 深水埗區6322.3303, 114.162215
Kwai Tsing / 葵青區9122.3570, 114.130014
Tsuen Wan / 荃灣區9222.3710, 114.114014
Tuen Mun / 屯門區9322.3910, 113.977014
Yuen Long / 元朗區9422.4445, 114.022014
Tai Po / 大埔區9522.4501, 114.168115
North / 北區9622.4940, 114.138013
Sha Tin / 沙田區9722.3820, 114.188014
Sai Kung / 西貢區9822.3810, 114.271013
ASCII Escaping / ASCII 轉義: The config is injected ASCII-escaped so Traditional Chinese characters survive regardless of how the output file is served. 配置內容會進行 ASCII 轉義(中文字符轉為 \uXXXX 格式),以確保繁體中文在地圖網頁分發時不會出現亂碼。

04CSV input templates / CSV 範本

Colleagues fill three 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. Columns are normalized using norm = h => (h||"").toLowerCase().replace(/[^a-z0-9]/g,"") to prevent space/case mismatches. Coordinates are validated browser-side to ensure they fall within standard Hong Kong spatial boundaries: Latitude 21.8 to 22.8 and Longitude 113.5 to 114.6; coordinates outside these bounds are skipped.

工作人員需填寫三個 CSV 數據範本(可從構建器下載)。由於坐標解析和部門分類是在客戶端瀏覽器驗證時進行的,因此 CSV 的原始內容會保留在瀏覽器內存中。這使得當用戶在自訂面板中修改部門前綴或徽章顏色規則時,Builder 能夠立即動態重新解析所有坐標並重繪前綴徽章,而無需重新上傳文件。表頭會通過 norm = h => (h||"").toLowerCase().replace(/[^a-z0-9]/g,"") 進行歸一化以排除空格或大小寫干擾。經緯度在瀏覽器端會進行邊界驗證,確保其在香港標準地理邊界(緯度 21.822.8,經度 113.5114.6)以內,超出者會被跳過。

Validation Note / 校驗提示: While the tables below indicate certain fields (such as Serial or Set Date) are conceptually required (✓) for correct presentation on the final maps, the builder code only strictly rejects files if essential identifiers (Camera ID, Gravidtrap No., Survey Area) or geographic coordinates are missing. 雖然下列表格標識了某些欄位(如 Serial 序號或 Set Date 放置日期)為邏輯必填(✓)以保證最終地圖正確渲染,但構建器代碼在校驗時,僅在缺失關鍵標識符(相機 ID、誘蚊器編號、調查分區)或經緯度坐標時才會中斷並報錯。

Camera template — one file per phase / 相機數據範本(每階段一份)

Column / 欄位Req. / 必填Format / 格式與數值
Camera IDCombined ID, e.g. TP_038, HD_TP_606, LCSD_TP_801混合相機 ID,包含部門前綴代碼與序列號。
Location Typefree text (Side lane, RCP, Garden …) 位置類型描述(如後巷、垃圾收集站、花園)。
Streetfree text 街道名稱。
Street No.free text 門牌號碼。
Building / Landmarkfree text (Traditional Chinese OK) 大廈名稱或地標(支援繁體中文)。
LatitudeWGS84 decimal (~22.x) WGS84 十進制緯度。
LongitudeWGS84 decimal (~114.x) WGS84 十進制經度。
RiskH / M / L / N.A. 風險等級。
RAR (%)number 0–100, or blank. Fractional inputs (e.g. 0.73) are auto-detected and scaled to percentages (e.g. 73%) with a warning. 無鼠百分比(0-100)。若留空,代表調查仍在進行中,相機在地圖上顯示為待調查顏色(預設橄欖綠色)。支援自動檢測並轉換小數/分數(如上傳的 0.73 會自動轉換為 73%)並發出提示警告。

Pool template — one file per district / 抽樣池範本(每區一份)

The pool layer is header-driven — latitude/longitude are found by column name, and every populated column appears dynamically in popup bubbles.

抽樣池圖層由表頭驅動,系統會自動定位經緯度列,並將所有已填寫的屬性動態呈現在彈出窗中。

Column / 欄位Req. / 必填Format / 格式與數值
SerialNumber, e.g. 1, 2序號。
District Cam Ref.Camera ID, e.g. TP_002分區相機參照編號。
Location Typefree text 位置類型描述。
Streetfree text 街道名稱。
Street No.free text 門牌號碼。
Building / Landmarkfree text 大廈或地標。
Lamp Postfree text, e.g. H1234燈柱編號。
Street Detailsfree text 街道詳情。
LatitudeWGS84 decimal (~22.x) WGS84 十進制緯度。
LongitudeWGS84 decimal (~114.x) WGS84 十進制經度。

Gravidtrap template — one file per district / 誘蚊器數據範本(每區一份)

The Latitude and Longitude columns reside at the end of the sheet. The Year column is optional (a year code indicates positive gravidtrap rounds, while an empty year represents negative gravidtrap rounds, with date-fallback auto-resolving the round's actual year).

經緯度放置在最後兩列,年份為選填(留空代表陰性,有填代表陽性,系統會通過日期自動解析年份以進行合併篩選)。

Column / 欄位Req. / 必填Format / 格式與數值
Gravidtrap No.Number, e.g. 1, 22誘蚊器編號。
Survey Areafree text, e.g. Tai Po East調查分區。
Year4-digit year, e.g. 2026, or blank 年份。選填,如果留空代表該輪調查為陰性,系統會自動根據放置日期或檢查日期解析實際年份。
MonthMonth abbreviation, e.g. Apr, May月份簡寫。
Address or Landmarkfree text 地址或地標描述。
Managed byfree text, e.g. FEHD, LCSD管理單位。
Set DateDate, e.g. 2026-04-28放置日期。
GI DateDate, e.g. 2026-05-05檢查日期。
No. of GI AdultNumber, e.g. 0, 2捕獲成蚊數量。
Latitude✓*WGS84 decimal (~22.x). *Only required once per gravidtrap (blank values automatically propagate coordinates from previous rounds). WGS84 十進制緯度。*每個誘蚊器僅需填寫一次,空白行會自動從其他填寫過經緯度的週期行中補全。
Longitude✓*WGS84 decimal (~114.x). *Only required once per gravidtrap (blank values automatically propagate coordinates from previous rounds). WGS84 十進制經度。*每個誘蚊器僅需填寫一次,空白行會自動從其他填寫過經緯度的週期行中補全。

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, Step 2.2, and Step 2.3) are hidden by default. Click five times in rapid succession anywhere on the Step 2 card to reveal Step 2.1 (Colors & RAR bands), Step 2.2 (Department badges), and Step 2.3 (Gravidtraps). 為保持界面整潔,高級自訂面板(步驟 2.1、2.2 和 2.3)默認隱藏。在 Step 2 卡片 的任意空白處快速點擊 5 次即可解鎖並展開 步驟 2.1(顏色與色彩頻譜)步驟 2.2(部門徽章) 以及 步驟 2.3(分區監測與誘蚊器上傳)
  • Floating Preview Legend / 地圖內懸浮圖例: The preview legend is styled as an absolute HTML div element in the bottom-right corner of the preview map. It dynamically updates based on the active preview selection (Pool vs. Surveillance vs. Result RAR). Note: To keep the builder simple, the preview map does not implement advanced spatial features like marker clustering, overlapping spiderfying (OMS), or coordinate propagation; these are only rendered in the final template product. Also, unlike the collapsed markers in the compiled product, the preview map displays every raw gravidtrap row as a separate marker. 預覽地圖內置了懸浮圖例(絕對定位 HTML div 元素),並會根據當前切換的預覽階段(抽樣池 vs. 待調查階段 vs. 結束階段)動態刷新。註:為保持構建器輕量,預覽地圖僅用於快速定位,不包含標記聚合(Clustering)、重疊展開(OMS)或歷史經緯度自動補全等高級空間特性,這些僅在生成導出的成品地圖中生效。同時,預覽地圖會將誘蚊器 CSV 的每一行渲染為獨立圖標,而非像最終成品地圖那樣按監測點位置進行合併折疊。

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 in CFG.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.departments prefix mapping dictionary. 氣泡窗中的部門徽章背景顏色通過對 CFG.departments 進行字典查找來動態著色。
  • Dynamic Legend Redrawing / 圖例動態生成: The map's legend draws its items dynamically from the config bands during base layer transitions. The legend is implemented as a styled absolute HTML div element container appended directly to the map object, rather than using Leaflet's built-in L.Control. 地圖加載和切換階段時,圖例會根據配置中的頻譜段動態重繪。圖例是由自訂的絕對定位 HTML div 容器直接附加到地圖元素上實現,並非使用 Leaflet 的內置 L.Control 控制器。
  • 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 to manage the rendering order. The hkLabels pane (zIndex: 250 with pointerEvents: 'none') places English and Chinese street labels directly above the base map tiles (zIndex: 200) but underneath vector lines (like lotPane at zIndex: 450, ringPane at zIndex: 420, and default vector overlayPane at zIndex: 400) and markers (default markerPane at zIndex: 600). This ensures street labels are visible against base tiles but do not overlap or clutter critical survey overlays. Note: custom panes are only implemented in the compiled map template, not in the builder's preview map.

為提供高質量的地圖背景,模板支援多種底圖瓦片圖層(CartoDB、非官方 Google Maps 以及地政總署官方地理空間數據 API)。它利用了 Leaflet 的自訂地圖窗格(Custom Panes)來排列層級渲染順序。自訂 hkLabels 窗格(zIndex: 250 與 pointerEvents: 'none')確保中英文街道地名標籤渲染在底圖瓦片(zIndex: 200)之上,但在地段邊界(lotPane 設 zIndex: 450)、緩衝環(ringPane 設 zIndex: 420)、普通向量圖層(zIndex: 400)以及地圖標記(zIndex: 600)之下,保證街道文字可見的同時不會遮擋核心業務標記與邊界數據。註:自訂窗格僅在編譯輸出的地圖模板中生效,Builder 本身的預覽地圖不包含此窗格設置。

// 1. CartoDB Gray Base Map (Default)
const lightMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
  attribution: '&copy; OpenStreetMap &copy; 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: '&copy; 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: '&copy; 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 but below markers/boundaries
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.
To render these polygons on the map, the coordinates of every vertex must be transformed from HK1980 Easting/Northing (E/N) to WGS84 latitude/longitude using the official Coordinate Transformation API. Because a single viewport contains hundreds of polygons with thousands of vertices, doing this naively would flood the network and trigger government rate-limiting blockages. The template overcomes this using two key designs:
  1. 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%.
  2. 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 幾何圖形即由此坐標構成。
為了在地圖上渲染這些多邊形,必須調用官方「坐標轉換 API」將每個頂點的東距/北距(E/N)轉換為 WGS84 經緯度。由於一個視窗中可能包含數百個多邊形和數千個頂點,直接發送所有請求會導致瀏覽器卡頓,並觸發政府服務器的流量限制(Rate Limiting)。地圖模板採用了以下兩種設計解決此問題:
  1. 頂點去重與快取 (lotVertexCache): 相鄰地段共享邊界頂點。模板將頂點坐標四捨五入至小數點後一位(10公分精度)並存入 Map 快取,減少了高達 90% 的 API 請求。
  2. 並發池控制 (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. Coordinate transformations (HK1980 Grid to WGS84) are handled client-side. Autocomplete suggestions query CSDI server directly (debounced by 250ms), and direct lamp post queries use SQL filter LAMP_NO = '[NO]' (falling back to CSDI WFS client-side property checking for robustness):

為實現地點搜尋、地址查詢以及燈柱定位,地圖模板整合了多個由政府地理空間數據門戶網站(geodata.gov.hk 及 portal.csdi.gov.hk)託管的服務。針對燈柱等超高密度的地理特徵,系統採用了視窗包圍盒(Viewport Bounding Box)動態分頁加載以防瀏覽器卡頓,並在客戶端進行坐標系統轉換(HK1980 網格坐標轉為 WGS84 經緯度)。自動完成搜尋直接在伺服器端查詢(250毫秒防抖),直接燈柱查詢採用 SQL 過濾件 LAMP_NO = '[NO]'(並以客戶端 WFS 特性校驗作為彈性備用):

// 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 code ' + data.ErrorCode);
  return [data.wgsLat, data.wgsLong]; // returns [lat, lng]
}

// 3. CSDI Viewport & Direct Lamp Post Queries (Services ID: hyd_rcd_1629267205229_84645)
const LAMPPOST_SERVICE_ID = 'hyd_rcd_1629267205229_84645';
// (A) Viewport Envelope Queries: geometry=west,south,east,north&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&inSR=4326
// Bounding-box SQL filter: `DISTRICT = '[District]'`
// (B) Direct Search SQL filter using LAMP_NO:
const where = `LAMP_NO = '${lampNo.replace(/'/g, "''")}'`;

// 4. CSDI Food Licence API Service IDs (with layered fallbacks and client-side filterTaiPo())
// 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]'" (falling back to client-side filtering on failure)

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, logging specific warnings with hostname details and throwing detailed errors showing the last proxy attempt error (lastErr):

為確保即使直連受限或公共代理超時,仍能成功加載食物牌照 GeoJSON 數據,模板實施了多代理輪詢與超時機制,在日誌中輸出具體代理主機名之超時警告,並在完全失敗時拋出包含最後一次嘗試之錯誤詳情(lastErr):

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...');
  }
  let lastErr = null;
  for (let round = 1; round <= PROXY_ROUNDS; round++) {
    for (const wrap of CORS_PROXIES) {
      try {
        const proxyUrl = wrap(url);
        const proxyHost = new URL(proxyUrl).hostname;
        const res = await fetchWithTimeout(proxyUrl, FETCH_TIMEOUT_MS);
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return await res.json();
      } catch (e) {
        lastErr = e;
        console.warn(`Proxy ${proxyHost} failed (round ${round}): ${e.message}`);
      }
    }
  }
  throw new Error('All CORS proxies failed — last error: ' + (lastErr ? lastErr.message : 'unknown'));
}

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 a styled absolute HTML container inside the map element rather than a static list below the map, the preview map presentation is identical to the generated product. 將圖例以絕對定位 HTML 容器內置於地圖元素的右下角,而非堆疊在下方,保證了預覽地圖與最終導出的網頁在呈現效果上完全相同。
  • 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 時顯示編號,大大節省了內存佔用並保持畫面整潔。
  • Phase-Optional Compilation / 階段無依賴編譯: Allows generating maps without any camera phases (mosquito-only maps). This decouples the Dengue mosquito surveillance layers from the camera system. The map template automatically hides camera cycle toggle buttons and search rows, though the range filter slide panel button remains clickable but non-functional. 階段無依賴編譯。支持在零相機監測階段的配置下編譯地圖(純蚊患圖層地圖)。地圖模板會自動隱藏相機切換按鈕與定位搜索欄;然而,篩選面板折疊按鈕仍可點擊展開,此時內部的無鼠百分比滑動條為無效狀態。
  • Two-Pass Coordinate Propagation / 雙階段經緯度補全機制: Solves data redundancy and fatigue by allowing blank coordinates for recurring gravidtraps. The builder maps the final coordinate to a lookup key during the first pass and populates all matching blanks during the second pass. 雙階段經緯度補全機制。解決歷史週期中重複錄入位置坐標的數據贅餘問題。系統在首輪掃描中收集最末一次出現的有效坐標,並在次輪掃描中一鍵補全剩餘週期中的經緯度空白。
  • Final Coordinate Alignment / 最新坐標對齊原則: Dictates that if a gravidtrap has different coordinates in different rows, the collapsed marker is plotted at the last entered coordinates in the CSV, reflecting the most up-to-date position. 最新坐標對齊原則。當同一個監測點在不同週期的記錄中被錄入了不同的坐標時,系統在地圖聚合渲染時會以 CSV 最下方(即最後錄入)的經緯度坐標作為該圖標的最終在地圖上的定位,確保位置信息最新。
  • "Once Positive" Hotspot Visibility / 歷史曾呈陽性篩選與熱點可見性: Adds a historical boolean check to allow users to preserve the visibility of gravidtraps that have been positive in any past round, serving as a powerful tool to monitor hotspot areas. 歷史曾呈陽性篩選與熱點可見性。新增歷史布林值篩選,使用戶可選擇篩選顯示在過去任何一期調查中呈陽性的誘蚊器。該功能為追蹤高風險熱點提供了強大支持,避免因單月陰性結果而忽視對潛在隱患點的監控。

10Running & viewing / 運行與部署

Local Server: The builder and template must be served from an http(s) origin (e.g., run python3 -m http.server 8000 in the folder) for the generate step to fetch the dynamic template file. The builder is accessed at http://localhost:8000/index.html. The generated map product can be run locally by double-clicking the exported file.

本地服務器需求: 由於「生成」步驟涉及瀏覽器端異步獲取(Fetch)地圖模板文件,Builder 必須在 HTTP(S) 協議環境下打開(例如在目錄下運行 python3 -m http.server 8000)。您可以在瀏覽器中訪問 http://localhost:8000/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, type) {
  const blob = new Blob([text], { type: type || "text/plain" });
  const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename;
  document.body.appendChild(a); a.click(); a.remove(); 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 utilize native HTML min="0" max="100" input constraints to enforce logical boundaries (0 to 100) on color band parameters. 色彩區間輸入具備防錯驗證,使用原生 HTML 的 min="0" max="100" 屬性以限制填寫的區間數值在合理界限(0 至 100 之間)內。

12Rebuild checklist / 重構檢查清單

If rebuilding this customizable system from scratch:

如果未來的開發人員需要從頭重建這個高度自訂化的系統,請遵循以下步驟:

  1. Define the Configuration Contract First / 首先定義數據合約: Ensure APP_CONFIG contains nested structures for custom global colors, range bands, and department badge colors. 設計配置架構,確保合約包含自訂色彩、色彩頻譜數組以及部門徽章對照字典。
  2. 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 粘性定位以在滾動時保持可見。
  3. 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 亂碼,並基於自訂字典正則表達式動態提取部門前綴。
  4. Enable Dynamic Legend Redrawing / 啟用動態圖例重繪: Implement styled absolute HTML elements within the map wrapper to draw the legend dynamically based on the configuration bands. 在地圖容器內實現自訂絕對定位的 HTML 元素,以讀取配置區間段動態生成並重繪圖例。
  5. 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
部門徽章
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. 由食環署部署於各行政分區的監測誘蚊器,用於登革熱傳播媒介監測,並在地圖上呈現最新一輪的陽性/陰性指數狀態。