map3d 完整教學 — 從 OSM bounding box 到可下載 GLB 的 R3F 工作流

本文件是 cartesiancs/map3d深度技術教學,配對的 gh-save 報告請見 2026-05-25-github-cartesiancs-map3d.md

建議閱讀順序:先讀本檔 §1–§2 建立心智模型,再依需求跳讀 §3(架構)/ §4(工作流細節)/ §6(資安)。


1. 專案定位

1.1 一句話

map3d 是「Leaflet 拉框 + OSM Overpass + R3F 即時 extrude + GLTFExporter 一鍵匯出」的純前端工具,把 GIS 工作流壓縮到 ~1,400 行 TypeScript。

1.2 設計品味(為何值得讀)

面向評價原因
依賴選擇🟢 好品味react-leaflet 處理 2D、@react-three/fiber (R3F) 處理 3D,職責清晰、不混用
資料流🟢 好品味bbox → Overpass query → element[] → ExtrudeGeometry,單向、無 round-trip
state 結構🟡 可接受3 個 Zustand store(area / car / export)分得乾淨,但 action 命名 setAction(true) 過於通用
error handling🔴 待改進catch (error) {} 空塊出現多處,第三方 API 失敗會默默吞掉
TypeScript 嚴謹度🔴 待改進: any 散落、onDone: (e) => void 缺型別
可讀性🟢 好品味單檔不超 510 行(最大檔 Space.tsx 508 行),章節分明

1.3 與同類工具的關係

1┌─────────────────────────────────────────────────────────┐
2│  3D Map / Digital Twin 工具光譜                          │
3├─────────────────────────────────────────────────────────┤
4│  CesiumJS / deck.gl     │ 重量級、付費 tile、GIS 嚴謹    │
5│  Mapbox GL 3D Tiles     │ 商用 API、需 access token      │
6│  ★ map3d                │ 輕量、純 OSM、可 fork、MIT     │
7│  Three.js 純造          │ DIY、需手寫 OSM parser         │
8│  OSMBuildings.js        │ 老牌、API 較陳舊、無 R3F 整合  │
9└─────────────────────────────────────────────────────────┘

map3d 的甜蜜點:「想要學 R3F + 真實資料整合、又不想付費 API」 的開發者。


2. 安裝指南

2.1 系統需求

  • Node.js ≥ 20.x(vite 6 + React 19 要求)
  • npm(package-lock.json 已 commit,推薦用 npm 避免鎖檔不一致)
  • 瀏覽器:Chrome 110+ / Firefox 110+(需支援 WebGL 2 + ES2022)

2.2 安裝 5 步驟

 1# 1. clone
 2git clone https://github.com/cartesiancs/map3d.git
 3cd map3d
 4
 5# 2. install
 6npm install
 7
 8# 3. dev server (vite, http://localhost:5173)
 9npm run dev
10
11# 4. build production
12npm run build
13
14# 5. preview
15npm run preview

2.3 安裝流程圖


flowchart TD
    A[git clone] --> B[cd map3d]
    B --> C[npm install]
    C --> D{選擇模式}
    D -->|開發| E[npm run dev]
    D -->|產品| F[npm run build]
    E --> G[localhost:5173]
    F --> H[dist.tar 部署到 CDN]
    G --> I[Step 0: 拉框]
    H --> J[線上 demo
map.fleet.im]

2.4 常見安裝問題

症狀原因解法
npm install 卡在 three 編譯Node < 18升級到 Node 20 LTS
WebGL context lost顯卡記憶體不足縮小 bbox 或關其他 GPU 應用
Leaflet 樣式破版忘記 import css已內建 import "leaflet/dist/leaflet.css"
跨域取 Overpass fail地區 ISP 擋 overpass-api.de改用 mirror(lz4.overpass-api.de / z.overpass-api.de

3. 核心架構解析

3.1 元件樹(component tree)


flowchart TB
    Main[main.tsx] --> App[App.tsx
狀態機 step=0/1/2] App --> TopNav[TopNav] App --> FMOdal[FullscreenModal] App --> Modal[Modal] App --> MapSelect[SelectMap.tsx
react-leaflet] App --> Process[Processing.tsx
建築清單] App --> Space[Space.tsx
R3F Canvas] Space --> Building[Building component
extrudeGeometry] Space --> Roads[Roads component
Line] Space --> Car[Car.tsx
WASD 漫遊] Space --> Export[Export component
GLTFExporter] MapSelect --> RectSel[RectangleSelector] App --> Stores{{Zustand stores}} Stores --> areaStore Stores --> carStore Stores --> exportStore

3.2 三層責任分配

元件責任行數
UI 流程App.tsx狀態機 (step) + 跨步驟 fetch Overpass305
2D 地圖SelectMap.tsxLeaflet MapContainer + RectangleSelector (滑鼠/觸控)254
3D 渲染Space.tsxR3F Canvas + Building/Roads/Export 子元件508
互動Car.tsxOrbitControls / PointerLock + WASD 物理150
狀態state/*.tsZustand store(area/car/export)59

3.3 資料流(end-to-end)


flowchart LR
    U[使用者拉框] -->|onChange
LatLngBounds| AS[areaStore
setCenter] AS --> APP[App handleClickNextStep] APP -->|POST query| OV[Overpass API
way&building / highway] OV -->|elements.json| APP APP -->|appendAreas| AS2[areaStore
areas] AS2 --> SP[Space.tsx
areaData()] SP --> BUILD[Building 元件
ExtrudeGeometry] SP --> ROAD[Roads 元件
fetch highway
並渲染 Line] BUILD --> CV[R3F Canvas] ROAD --> CV CV -->|userData.exportToGLB| EX[Export 元件
GLTFExporter] EX -->|blob| DL[scene.glb 下載]

3.4 投影函式(latlng → 平面座標)

map3d 用一個極簡的「等距圓柱投影 (equirectangular)」把經緯度轉成 R3F 場景座標:

1const scale = 51000;  // 經驗值,使距離視覺合理
2
3function project(lat: number, lng: number) {
4  const x = (lng - refLng) * scale * Math.cos((refLat * Math.PI) / 180);
5  const y = (lat - refLat) * scale;
6  return new THREE.Vector2(x, y);
7}

refLat / refLng 取 bbox 中心,所以「選的區域中心永遠在 scene 原點」。Math.cos(refLat) 修正經度隨緯度收斂的問題(赤道 1° lng ≈ 111km,極區趨近 0)。

限制:scale = 51000 是 hard-coded,跨大區塊(> 0.1°)誤差會累積、建築物變形。README 內建「太大警告 modal」就是這原因。

3.5 高度估計(building.tags → extrude depth)

1let heightValue = parseFloat(bld.tags.height || "");
2const heightLevels = parseFloat(bld.tags["building:levels"] || "");
3if (isNaN(heightValue)) heightValue = 10;          // 預設 10m
4if (!isNaN(heightLevels)) heightValue = heightLevels * 2.2;  // 每層 2.2m

優先序:tags.height > tags.building:levels × 2.2 > fallback 10m(issue #5 / #12 的「都是立方體」就是這 fallback 觸發)。


4. Helper 程式與關鍵檔細解

4.1 App.tsx — 三步驟狀態機

1const [steps, setSteps] = useState(["front", "processing"]);  // 預設 2 步
2const [step, setStep] = useState(0);

注意:實際 UI 是「拉框 → Processing → 3D Canvas → Export」4 步,但 steps 陣列只列 2 個(front / processing)。第 3 步「3D Canvas」是當 step==2<FullscreenModal> 全部 isOpen=false<Space> 顯露的 implicit 狀態。這是專案最 tricky 的設計選擇,初讀容易誤解。

關鍵函式:

函式用途
handleDone(data)Leaflet 拉框完成後呼叫,把 [NE, SW] 寫入 areaStore.center
requestBuildings()POST Overpass,回傳 element[],filter geometry 後 appendAreas
checkIsBig()(lat_a - lat_b) + (lng_a - lng_b) > 0.1 → 太大警告
checkFleetLogin()讀 cookie token,決定是否顯示 Fleet 整合 UI

4.2 Space.tsx — R3F 場景 (508 行最大檔)

3 個子 component:

  1. Building(行 13–313):mesh + extrudeGeometry + 互動 popup(HTML 元素疊在 3D mesh 上方)。
  2. Roads(行 315–368):useEffect 內 fetch Overpass way["highway"],把每條路畫成 <Line>
  3. Export(行 370–440):scene.traverse 過濾 userData.exportToGLB === true 的 mesh → GLTFExporter → blob → 下載或 upload。

設計巧思:Building / Roads / Car 三類 mesh 都帶 userData.exportToGLB = true,Export 時只挑這些。其他輔助 mesh(Sky / Environment / Light)不會被匯出。這比「全 scene 匯出」更乾淨

4.3 Car.tsx — 第三人稱漫遊

  • WASD 鍵盤控制(用 keys.current.w/s/a/d ref 避免 re-render)
  • Pointer Lock 滑鼠視角(FPS 標準作法)
  • 加速度 / 減速度 / 最大速度寫死(accelerationRate=0.2 / maxSpeed=3.0 / decelerationRate=1.0
  • useFrame 內每 frame 更新 position + 攝影機 lerp(0.1 平滑跟隨)

4.4 state/*.ts — Zustand 範本

1// areaStore.ts (28 行)
2export const useAreaStore = create((set) => ({
3  center: [{lat:0,lng:0},{lat:0,lng:0}],
4  areas: [],
5  setCenter: (data) => set(() => ({ center: data })),
6  appendAreas: (data) => set((state) => ({ areas: [...state.areas, ...data] })),
7}));

Zustand 用法極乾淨,沒有 immer / persist / devtools middleware,對初學者是好教材

4.5 api/axios.ts — Fleet 整合(10 行)

1const instanceFleet = axios.create({
2  baseURL: `https://api.fleet.cartesiancs.com/api/`,
3  timeout: 7000,
4  headers: { "x-access-token": getCookie("token") },
5});

重要:這是 maintainer 自家服務 fleet.cartesiancs.comfork 後可移除 / 自行替換。當前版本 App.tsx 行 273–284 的 Fleet 登入 UI 已被註解掉(commit 935067f “remove request button”),表示作者正在「降低自家服務的耦合」。


5. 應用場景

5.1 直接使用(不改 code)

  1. 教學 demo:給學生看 R3F + OSM + GLTFExporter 完整鏈
  2. 快速建模:城市區塊原型,匯出 GLB → Blender 後製
  3. Drone 模擬:把區塊匯出當 Unity / Unreal 飛行訓練環境

5.2 二次開發場景

場景改什麼預估工時
加入屋頂類型(roof:shape)Space.tsx Building component 內判斷 tags.roof:shape 切換 geometry4–8h
改用 Mapbox 3D tiles 取代 extrude<Building> 為 Mapbox SDK,保留 export 邏輯16h
加入高度紋理(texture)meshStandardMaterial 改為 meshPhysicalMaterial + textureLoader4h
大區塊分塊 fetchApp.tsx requestBuildings 改為 grid 切割8h
移除 Fleet 整合(純 self-host)api/axios.ts + App.tsx 行 22–84 + 273–2841h
改投影為 Web MercatorSpace.tsx project() 重寫4h(含驗證)

5.3 整合進其他工作流

  • bioinformatics + 城市流行病:把人口稠密區塊 export 成 GLB → 餵 Unreal Engine 視覺化 SARS / 流感擴散
  • 建築 AI 訓練資料:批次 fetch + export 100 個城市 → image rendering → diffusion model 訓練
  • AR / WebXR:把 GLB 載進 <model-viewer><a-scene> 顯示城市縮影

6. 資安掃描報告

6.1 評級

🟢 整體低風險(適合學習 / fork / 自架)

map3d 是純前端 SPA、無後端、無 secret。掃描全 codebase 後沒發現任何高風險 pattern(eval / exec / dangerouslySetInnerHTML / 硬編 secret 都不存在)。

6.2 詳細掃描結果

項目狀態細節
eval / Function() / exec🟢全 codebase 0 次出現
dangerouslySetInnerHTML🟢0 次出現;HTML overlay 用 <Html> (drei) 安全注入
硬編 secret / API key🟢0 次出現;只有公開 OSM / Fleet API URL
shell / subprocess🟢N/A(純前端)
第三方 CDN 無 SRI hash🟡src/index.css:1 從 jsdelivr 拉 pretendard 字型 → 建議加 integrity=
document.cookie 直接操作🟡utils/cookie.ts 讀寫 cookie,無 HttpOnly / SameSite 屬性(仍由瀏覽器設)
React key={index}🟡Space.tsx:493 等多處用 index 當 key,動態增減時可能 re-mount 異常
catch (error) {} 空 catch🟡App.tsx:105 等多處吞錯誤,debug 困難
as any / 隱式 any🟡App.tsx:75 getSpace: any、多處 callback 缺型別
Overpass API 無 rate-limit 保護🟡高頻拉框時可能被 ban;建議加 debounce
GLB 上傳 (Fleet) 缺 CSRF🟡但 Fleet UI 當前已註解掉,預設不會觸發

6.3 部署建議

  1. 靜態託管即可(GitHub Pages / Cloudflare Pages / Vercel),無需後端伺服器
  2. Content Security Policy (CSP)
    1default-src 'self';
    2img-src 'self' https://*.tile.openstreetmap.org data:;
    3connect-src 'self' https://overpass-api.de https://api.fleet.cartesiancs.com;
    4style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
    5font-src https://cdn.jsdelivr.net;
    
  3. 若拿掉 Fleet:上面 CSP 可移除 api.fleet.cartesiancs.com

6.4 隱私

  • 使用者選的 bbox 會送到 overpass-api.de(公開服務、無 PII)
  • 本地 cookie token 只在登入 Fleet 時建立,未登入 = 完全離線
  • 無 analytics / tracking

7. FAQ

Q1:為什麼下載的 GLB 是一個立方體? A:OSM 該區域的 building 沒有 geometry 或 height tags,map3d fallback 成預設立方體(10m × footprint)。換到歐美城市試(如 New York / Berlin)會明顯改善。

Q2:可以離線使用嗎? A:不行 — Overpass API 是 live query。離線需要:(1) 自架 Overpass instance(Docker wiktorn/overpass-api),(2) 改 fetch URL 指向 localhost。

Q3:可以選圓形區域、多邊形區域嗎? A:當前 UI 只支援矩形 bbox。要改成 polygon 需重寫 SelectMap.tsxRectangleSelector 元件、改 Overpass query 為 (poly:"...") 語法。

Q4:地球曲率影響大嗎? A:在 < 1km × 1km 範圍可忽略;> 10km × 10km 建築物會「歪斜」。map3d 內建警告 0.1° ≈ 11km。

Q5:能商用嗎? A:MIT License — 可以。需保留版權聲明。OSM 資料受 ODbL(Open Database License),若重新發佈資料需附 attribution;只匯出 GLB 自用無此限制。

Q6:為什麼 react-leaflet + R3F 兩個 canvas 不衝突? A:Leaflet 用 DOM + SVG/Canvas2D,R3F 用 WebGL;兩者是不同的 rendering context。同畫面可並存。


8. 進階技巧

8.1 加速大 bbox(用 Overpass API 的 bbox-around)

1// 原寫法:way["building"](south,west,north,east)
2// 加速:先 query node[building=*],再 expand to way
3const query = `[out:json][timeout:60];
4  way["building"](${south},${west},${north},${east});
5  out body geom 5000;`;  // 限制最多 5000 棟,避免 timeout

8.2 加 building footprint cache

1// Zustand persist middleware
2import { persist } from "zustand/middleware";
3
4export const useAreaStore = create(persist(
5  (set) => ({ /* ... */ }),
6  { name: "map3d-area-cache" }
7));

8.3 改用 meshPhysicalMaterial 加玻璃感

1<meshPhysicalMaterial
2  color="#9da0a3"
3  metalness={0.3}
4  roughness={0.4}
5  clearcoat={0.5}
6  transmission={0.1}  // 透光感
7/>

8.4 加陰影(shadow)

1<Canvas shadows camera={{ fov: 90 }}>
2  <directionalLight position={[10, 20, 5]} intensity={1} castShadow />
3  <mesh castShadow receiveShadow>{/* ... */}</mesh>
4</Canvas>

9. 整合進其他工作流

9.1 配合 docling(layer 8)

把 OSM building list 匯出成 markdown 表格 → docling 解析 → 餵給 paper-qa-lite 做「城市 X 的建築統計」問答。

9.2 配合 graphify(layer 4)

對 map3d 跑 graphify init . 可產出元件呼叫圖;對 fork 後做架構分析很有用。

9.3 配合 paper-tutorial(layer 15)

若閱讀「3D city modeling」相關 paper,可以把 paper 提到的 building footprint 演算法用 map3d 跑 visual demo,做成教學 HTML。


10. 重點摘要 Checklist

  • 核心命題:純前端 + OSM + R3F + GLTFExporter,1,400 行做完 city → 3D → GLB
  • 3 個進入點App.tsx(流程)/ Space.tsx(3D)/ SelectMap.tsx(2D)
  • 資料源:OSM Overpass(公開、無 key、有 rate limit)
  • 狀態管理:Zustand 三個 store(area / car / export),無 middleware
  • 匯出機制userData.exportToGLB = true flag + GLTFExporter
  • 限制 1:bbox < 0.1°(~11km)
  • 限制 2:缺 height tag 的 building → fallback 10m 立方體
  • 限制 3:必須線上(Overpass live)
  • 資安:🟢 低;建議加 CSP + SRI + Overpass debounce
  • 授權:MIT;可商用、可 fork、可改造

11. 進一步閱讀

主題資源
R3F 入門https://docs.pmnd.rs/react-three-fiber/getting-started/introduction
drei 元件庫https://github.com/pmndrs/drei
Three.js GLTFExporterhttps://threejs.org/docs/#examples/en/exporters/GLTFExporter
OSM Overpass QLhttps://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL
Overpass turbo(線上 IDE)https://overpass-turbo.eu/
OSM building taghttps://wiki.openstreetmap.org/wiki/Key:building
Leaflet 官方教學https://leafletjs.com/examples.html
Zustand 範例https://github.com/pmndrs/zustand
glTF 規格https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
map3d 線上 demohttps://map.fleet.im/

文件版本:2026-05-25,自動生成 by gh-tutorial-qd (Layer 12)。配對 gh-save 報告:2026-05-25-github-cartesiancs-map3d.md