原始圖資怎麼變成可視化地圖 & Maplibre Render Pipeline
從一份靜靜躺在硬碟裡的 .shp 檔案,到使用者眼前那張流暢縮放的互動地圖,中間到底發生了什麼事?這篇文章帶你一步步拆解整條 Tile Pipeline。
Created
Mar 21, 2026
發生了什麼事情?
剛接觸地圖開發的時候,我拿到一份公司的圖資 .shp 檔案,打開一看……就是一堆座標跟屬性,完全不知道怎麼「放到地圖上」。
地圖資料要怎麼從原始圖資變成一張精美的地圖,有各種地形/道路/POI?
下面是我從 0-1 幫公司建立的地圖最終成果 ٩(๛ ˘ ³˘)۶
整條 Pipeline 長這樣
中間有一整條 Pipeline 在默默運作,讓我們一層一層把它拆開來看!
Loading architecture diagram...
原始圖資 — .shp File
.shp (Shapefile) 是 GIS 世界最常見的向量資料格式,一份 Shapefile 其實是一組檔案的集合:
| 副檔名 | 存什麼? |
|---|---|
.shp |
幾何圖形(點、線、面) |
.dbf |
屬性資料(名稱、代碼等) |
.prj |
座標系統定義 |
.shx |
幾何索引 |
問題來了,.shp 只是一份完整的圖資,直接丟給前端完全不實際,光是台灣縣市邊界的資料可能就幾十 MB,更何況全球道路網?所以我們需要把它切成「Tile (磚塊)」。
Convert to Tile Database
.shp 檔案是一份完整的圖資,裡面有整個台灣所有的道路、建築物、行政區邊界……全部都在裡面。
假設你直接把這份資料丟給瀏覽器,會發生什麼事?
- 使用者打開地圖,瀏覽器開始下載……幾百 MB 的資料
- 等下載完才能看到地圖
- 而且你現在只是在看台北市的某條街,台南的資料你根本用不到,卻還是全部下載了
解法是把整份圖資切成一小塊一小塊,每一塊叫做一個 Tile(地圖磚)。
你可以想像成把一張超大的世界地圖海報,剪成幾千片拼圖碎片。使用者的瀏覽器只需要跟只跟伺服器拿目前範圍的那幾個 Tile。
而且,同一個地方在不同縮放層級下,對應的是不同解析度的磚片:
- 縮到很小(看整個台灣)→ 細節很少,每塊 Tile 的資料量小
- 放大到街道級別 → 細節很多,才載入那個小範圍的高精度資料
所以同一份原始圖資,要預先切出好幾套不同精細度的 Tile,統一存進一個資料庫,這個資料庫格式通常叫做 MBTiles。
因為目前沒有好工具可以直接將 shp 轉成 Tile,所以需要固定經過兩個步驟。
第一個步驟,使用 ogr2ogr 工具將 shp 轉成 Geojson 檔案 。
# Step 1:格式轉換
# ogr2ogr 是一個格式轉換工具,幾乎支援所有 GIS 格式互轉
# 這行的意思:把 input.shp 轉成 GeoJSON 格式,同時統一座標系統
ogr2ogr -f GeoJSON output.geojson input.shp -t_srs EPSG:4326
# ↑輸出格式 ↑輸出檔案 ↑輸入檔案 ↑目標座標系統(全球通用經緯度)
*座標系統: 不同的座標系統下,相同的經緯度實際是不同的地點,所以要確保 convert 的座標系統跟 shp 用同一種。
*GeoJSON 是一種基於 JSON 的地理資料格式,GeoJSON 的基本單位是 Feature。
{ "type": "Feature", //固定是 Feature "geometry": { //幾何資料(位置 / 形狀) "type": "Point", "coordinates": [121.543, 25.033] }, "properties": { //附加資料(商業邏輯用) "name": "Taipei 101" } }
第二個步驟,使用 tippecanoe 工具將 Geojson 轉成 Tile 檔案 。
# Step 2:切 Tile
# tippecanoe 是 Mapbox 出的切 Tile 工具
# 這行的意思:讀取 GeoJSON,切成各縮放層級的 Tile,輸出成 MBTiles 資料庫
tippecanoe -o tiles.mbtiles -zg --drop-densest-as-needed output.geojson
# ↑輸出檔案 ↑自動決定最佳縮放層級範圍 ↑太密集的地方自動簡化
在說 tippecanoe 做了什麼之前,要先搞懂地圖的縮放層級這個概念。
地圖服務用一個數字 z 來代表「你現在縮放到哪個層級」:
z = 0:整個地球只有一塊 Tile,解析度極低,只能看到洲的輪廓z = 1:地球切成 4 塊(2×2)z = 2:切成 16 塊(4×4)- 每增加一層,每個方向再切兩倍,所以 Tile 數量是
4^z z = 14:已經精細到能看清楚街道,全球共有 268 億塊 Tile
你打開 Google Maps 放大查看台北市某間建築z就會變大,用滾輪縮放的時候,背後就是在切換不同的 z,然後載入對應那個層級的 Tile。
問題來了:同一條台北市的道路,在 z = 5(看整個台灣)和 z = 16(街道細節)下,需要的精細程度完全不一樣。
z = 5的時候,台北市在螢幕上可能只有一個指甲蓋大,道路根本不需要顯示,只要看到縣市輪廓就好z = 10的時候,城市範圍看得到了,主要幹道可以出現,小巷不用z = 16的時候,放大到街道層級,每一個轉角的弧度都要準確
所以 tippecanoe 做的事情是:針對每一個 zoom level,產出一份對應精細度的資料,然後把所有層級的資料打包進同一個 MBTiles 資料庫。
MBTiles 資料庫內部長這樣(概念上):
z=0 → 1 塊 Tile → 每塊只有極度簡化的洲際輪廓
z=5 → 1024 塊 → 國家邊界、主要河流
z=10 → 100萬塊 → 城市道路網、建築群
z=14 → 10億塊 → 街道、個別建築輪廓
有了這個結構,任何一塊 Tile 都可以用三個數字精確定位:
- z:縮放層級(我要哪個精細度?)
- x:水平位置(左右第幾格?)
- y:垂直位置(上下第幾格?)
這就像試算表的欄列概念:z=2/x=3/y=1 就是第 2 縮放層級、第 3 欄、第 1 列的那塊 Tile,全球唯一,沒有歧義。
MBTiles 資料庫裡面就是用 (z, x, y) 當作索引存好每一塊 Tile 的資料,之後 Tile Server 收到 /tiles/14/13736/7000 這種 Request,直接去資料庫查,毫秒內就能回傳。( •̀ ω •́ )✧
tippecanoe 切出來的每塊 Tile,格式是 Protobuf(.pbf),也叫做 Mapbox Vector Tile(MVT),這是一種二進位的壓縮格式。
為什麼不存成 JSON?因為每塊 Tile 都要透過網路傳輸,體積越小越好。JSON 是純文字,光是一個 {"type":"Feature","geometry":{"type":"LineString","coordinates":[[...]]}} 就很長;Protobuf 把同樣的資料編碼成二進位,體積可以小好幾倍。
後面的 Step 會再細說 Client 怎麼使用 Tile 資料。
Host by Tile Server
有了 MBTiles 資料庫,需要一個 Tile Server 對外暴露 HTTP API,讓 Client 可以用座標來索取特定的 Tile。
常見選擇有 Martin、TileServer GL,基本上都能做到:
GET /tiles/{z}/{x}/{y}.pbf
Tile Server 就是去 MBTiles 裡面找到對應 z/x/y 的那一筆資料,塞進 HTTP Response 回傳出去,就這樣。架構超單純,擴容也容易。
Client Request x/y/z
有了 Tile Server 後我們前端就可以根據目前視窗範圍去打 Request 就可以拿到特定範圍的地圖 Tiles。
但是地圖的操作與渲染相當麻煩,因此我們直接使用開源套件 MapLibre GL JS 。
這個套件可以幫我們偵測目前視窗對應的 z/x/y 然後打 Request 給我們的 Tile Server 並自動解析 Tile 以 WebGL 繪製到我們的網頁上。
MapLibre Decode Protobuf → JSON
MapLibre GL JS 收到 .pbf 檔案後,會在 Worker Thread 裡解碼 Protobuf,還原成內部可以操作的 Feature 資料結構,這樣就不會卡住主執行緒的 UI。
Raw .pbf binary
↓ (pbf decoder)
Layer[] {
name: "road",
features: [
{ geometry: [[x,y], ...], properties: { class: "motorway" } },
...
]
}
解完之後的資料會進入渲染 pipeline,等待 Style JSON 來告訴它「長什麼樣子」。
Combine with Style JSON to Render WebGL
這是整條 Pipeline 最「看得見」的一步!
Style JSON 定義了地圖的視覺規則:哪個 Layer 要畫?什麼顏色?線條多粗?文字大小多少?MapLibre 把解碼後的 Feature 資料 + Style JSON 的規則交給 WebGL,在 GPU 上渲染出最終畫面。
{
"layers": [
{
"id": "road-motorway",
"type": "line",
"source-layer": "road",
"filter": ["==", "class", "motorway"],
"paint": {
"line-color": "#fc8d62",
"line-width": 4
}
}
]
}
WebGL 用 Shader 把這些幾何資料直接畫到 GPU,所以縮放、旋轉才能那麼流暢——這些操作都不需要重新跑 CPU 邏輯,只是調整 MVP 矩陣而已。( •̀ ω •́ )✧
完整流程回顧
| 步驟 | 做什麼 | 關鍵技術 |
|---|---|---|
| SHP → Tile DB | 切片、簡化、打包 | ogr2ogr, tippecanoe |
| Tile DB → Tile Server | 對外暴露 REST API | Martin, TileServer GL |
| Client 請求 Tile | 計算視窗對應的 z/x/y | MapLibre 內部邏輯 |
| Protobuf 回傳 | 高壓縮二進位傳輸 | MVT / .pbf |
| Decode → 資料結構 | Worker Thread 解碼 | pbf decoder |
| WebGL 渲染 | GPU 繪製向量圖形 | WebGL + Style JSON |
總結
如果你也有在玩地圖相關的開發,強烈建議直接用 MapLibre GL JS + Martin Tile Server 搭配,開源、免費、社群活躍,前人坑也踩的差不多了。(・∀・)つ
Continue reading