sean.
返回筆記列表

原始圖資怎麼變成可視化地圖 & Maplibre Render Pipeline

從一份靜靜躺在硬碟裡的 .shp 檔案,到使用者眼前那張流暢縮放的互動地圖,中間到底發生了什麼事?這篇文章帶你一步步拆解整條 Tile Pipeline。

建立日期

2026/3/21

01

發生了什麼事情?

剛接觸地圖開發的時候,我拿到一份公司的圖資 .shp 檔案,打開一看……就是一堆座標跟屬性,完全不知道怎麼「放到地圖上」。

地圖資料要怎麼從原始圖資變成一張精美的地圖,有各種地形/道路/POI?

下面是我從 0-1 幫公司建立的地圖最終成果 ٩(๛ ˘ ³˘)۶

02

整條 Pipeline 長這樣

中間有一整條 Pipeline 在默默運作,讓我們一層一層把它拆開來看!

Loading architecture diagram...

03

原始圖資 — .shp File

.shp (Shapefile) 是 GIS 世界最常見的向量資料格式,一份 Shapefile 其實是一組檔案的集合:

副檔名 存什麼?
.shp 幾何圖形(點、線、面)
.dbf 屬性資料(名稱、代碼等)
.prj 座標系統定義
.shx 幾何索引

問題來了,.shp 只是一份完整的圖資,直接丟給前端完全不實際,光是台灣縣市邊界的資料可能就幾十 MB,更何況全球道路網?所以我們需要把它切成「Tile (磚塊)」。

04

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 資料。

05

Host by Tile Server

有了 MBTiles 資料庫,需要一個 Tile Server 對外暴露 HTTP API,讓 Client 可以用座標來索取特定的 Tile。

常見選擇有 MartinTileServer GL,基本上都能做到:

GET /tiles/{z}/{x}/{y}.pbf

Tile Server 就是去 MBTiles 裡面找到對應 z/x/y 的那一筆資料,塞進 HTTP Response 回傳出去,就這樣。架構超單純,擴容也容易。

06

Client Request x/y/z

有了 Tile Server 後我們前端就可以根據目前視窗範圍去打 Request 就可以拿到特定範圍的地圖 Tiles。

但是地圖的操作與渲染相當麻煩,因此我們直接使用開源套件 MapLibre GL JS

這個套件可以幫我們偵測目前視窗對應的 z/x/y 然後打 Request 給我們的 Tile Server 並自動解析 Tile 以 WebGL 繪製到我們的網頁上。

07

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 來告訴它「長什麼樣子」。

08

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 矩陣而已。( •̀ ω •́ )✧

09

完整流程回顧

步驟 做什麼 關鍵技術
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
10

總結

如果你也有在玩地圖相關的開發,強烈建議直接用 MapLibre GL JS + Martin Tile Server 搭配,開源、免費、社群活躍,前人坑也踩的差不多了。(・∀・)つ

繼續閱讀