Protomaps Basemap 圖磚

完整的 Protomaps Basemap(縮放層級 0 至 15),以單一 PMTiles 封存檔透過標準 HTTP 提供。將任何 PMTiles 用戶端指向該 URL 即可運作。

https://tiles.jasontally.com/basemap.pmtiles

遇到 OpenStreetMap 圖磚被封鎖,或收到來自 tile.openstreetmap.org 的 403 錯誤嗎?從這裡開始。

試用

同一個封存檔提供三種樣式。切換樣式時,已載入的圖磚會保留在原處,因此能瞬間完成切換。

z0

自訂樣式

修改顏色或關閉圖層時,地圖會即時更新。接著複製樣式 JSON,這是一般的 MapLibre 樣式,你可以自行託管。它指向這些圖磚,因此無論放在哪裡都能正常運作。

使用方式

此封存檔支援 HTTP 範圍請求,因此所有 PMTiles 用戶端都能讀取。

一、在網頁中嵌入地圖

複製此檔案。這就是完整的範例:三個標籤、一個樣式 URL、一張地圖。兩個函式庫皆已鎖定版本並採用 BSD-3-Clause 授權;如果你不想使用 CDN,這台主機也有提供相同的檔案。

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Basemap</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css">
  <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/pmtiles@3.2.1/dist/pmtiles.js"></script>
  <style>html, body, #map { margin: 0; height: 100%; }</style>
</head>
<body>
  <div id="map"></div>
  <script>
    // Teach MapLibre how to read a PMTiles archive.
    const protocol = new pmtiles.Protocol();
    maplibregl.addProtocol("pmtiles", protocol.tile);

    new maplibregl.Map({
      container: "map",
      style: "https://tiles.jasontally.com/styles/light.json",
      center: [2.35, 48.85],   // Paris
      zoom: 4
    });
  </script>
</body>
</html>

二、替換現有樣式中的圖磚

每個 MapLibre 樣式都會從資料來源讀取圖磚。若要使用這些圖磚,只需修改該處的 url。樣式中的其他內容都不必更動。

  "sources": {
    "protomaps": {
      "type": "vector",
-     "url": "pmtiles://https://demo-bucket.protomaps.com/v4.pmtiles",
+     "url": "pmtiles://https://tiles.jasontally.com/basemap.pmtiles",
      "attribution": "<a href=\"https://www.openstreetmap.org/copyright\">&copy; OpenStreetMap contributors</a>"
    }
  }

或者透過程式碼修改,如果樣式來自你無法控制的伺服器,這種做法很實用:

// Rewrite every PMTiles source in a style to point at these tiles.
const style = await (await fetch("https://your-server/style.json")).json();

for (const source of Object.values(style.sources)) {
  if (source.url?.startsWith("pmtiles://")) {
    source.url = "pmtiles://https://tiles.jasontally.com/basemap.pmtiles";
  }
}

new maplibregl.Map({ container: "map", style, center: [2.35, 48.85], zoom: 4 });

三、放置自訂樣式的位置

上方產生器中的樣式 JSON 按鈕會開啟目前地圖正在繪製的樣式(包含你的修改),對話框內有複製按鈕。產出的內容有兩種使用方式,兩者都是一般網頁。

將它存成與網頁同目錄的檔案,並將 style 指向該檔案,就像這台主機上的三種樣式一樣。如果樣式屬於你自己的網站並會隨網站調整,請選擇這種方式:

// Save the copied JSON as my-basemap.json beside this file.
new maplibregl.Map({
  container: "map",
  style: "my-basemap.json",           // relative, same origin
  center: [2.35, 48.85],
  zoom: 4
});

或者直接放進 HTML 裡,這樣只需搬移單一檔案,無須額外託管樣式檔。樣式內容相同,只是存放位置不同:

// The copied JSON, pasted straight into the page.
const myStyle = {
  "version": 8,
  "name": "Protomaps Basemap — custom",
  "glyphs": "https://tiles.jasontally.com/font/{fontstack}/{range}.pbf",
  "sprite": "https://tiles.jasontally.com/sprites/v4/light",
  "sources": { /* ... as copied ... */ },
  "layers":  [ /* ... as copied ... */ ]
};

new maplibregl.Map({ container: "map", style: myStyle, zoom: 4 });

請保留 glyphs 與 sprite 這兩行不變。它們負責繪製文字標籤與圖示,並且指向這台主機,因此貼到你網頁中的樣式無論從哪裡提供服務都能正常運作。如果你完全不想依賴這台主機,可以改將它們指向 protomaps.github.io/basemaps-assets。

替換前請先確認一件事。此封存檔採用 Protomaps 結構描述 v3。為其他結構描述撰寫的樣式雖然會載入,但不會繪製任何內容,因為來源圖層名稱不相符。下方列出九個圖層名稱,以及可供複製的現成 v3 樣式。

四、字型、圖示集與函式庫

一張地圖包含圖磚、用於文字的 glyphs URL、選用的圖示 sprite URL,以及負責繪製的函式庫。這裡依各自預期的路徑提供所有資源,因此只要你願意,整張地圖可以來自單一主機名稱。

項目本站 URL授權條款

MapLibre 與 PMTiles 的檔案同時存放於 jsDelivr 與本站。兩個 URL 都能使用。

本頁並未使用最後一列的檔案。那是 Leaflet 的向量轉譯器,發布於此是為了讓你只需修改一個 script 標籤即可完成替換。Leaflet 本身則維持由其官方 CDN 提供。

提供 Noto Sans Regular 與 Noto Sans Medium 兩組字型堆疊,依 MapLibre 的要求拆分為 256 個區段,每段包含 256 個碼位。這裡發布了全部 256 個區段,而非僅限拉丁字母:Protomaps 會以當地語言標示地理要素名稱,因此東京或開羅需要數萬以上的碼位區段。若只發布英文地圖用到的區段,那些標籤就會無聲無息地消失,且不會顯示任何錯誤訊息。

五組圖示集(sprite sheet),對應上游的五種配色風格,皆提供 1x 與 2x 解析度。下方的樣式會從中讀取並繪製圖示。如果你想在自訂樣式中加入城鎮點、公路盾牌或地點圖示,請將 sprite 指向對應風格的圖示集。圖示集內的圖示名稱列在該圖示集的 light.json 中。

地圖繪製的內容與各項資料來源

每個圖層都來自此封存檔中的九個來源圖層之一,因此不需要第二個封存檔。其中有兩項值得特別說明,因為它們看起來像缺漏了資料,實則不然:

圖層資料來源包含於此版本?
地形輪廓、道路、建築物、水域、陸地earth, landcover, landuse, water, roads, buildings有
地名、路名與水域名稱上述圖層中的 name 欄位有
地點圖示、城鎮點pois.kind、places 以及圖示集有
道路編號盾牌roads.shield_text無。目前提供的建置版本沒有該欄位
門牌號碼buildings.kind=address 與 buildings.addr_housenumber無,原因相同

這裡沒有獨立的地址封存檔,也沒有額外的地址圖層需要讀取。上游專案將地址點直接嵌入 buildings 圖層中,作為帶有 kind=address 與 addr_housenumber 的圖徵。該欄位是在比目前地圖所用版本更新的 Protomaps 建置中才加入,因此門牌號碼圖層已經預先寫好,等下次每月更新匯入資料後就會自動顯示。道路編號盾牌文字也是如此:目前的版本僅有 shield_text_length(只記錄路線編號有幾個字元,而非編號內容),若據此繪製盾牌,每條高速公路上都只會出現空白圖案。

自行撰寫圖層與設定顏色

一個圖層由篩選條件、縮放層級、幾何形狀與顏色組成。篩選條件中有兩個常見陷阱,而且出錯時都不會顯示錯誤訊息:

以下範例包含一條道路、路名,以及關閉線條的開關:

{
  "id": "road-major",
  "type": "line",
  "source": "protomaps",
  "source-layer": "roads",            // one of the nine
  "minzoom": 4,                                  // not drawn below this
  "filter": ["==", ["get", "kind"], "major_road"],
  "layout": {
    "line-cap": "round",
    "visibility": "visible"          // "none" to hide it
  },
  "paint": {
    "line-color": "#f7c44d",          // any CSS colour
    "line-width": ["interpolate", ["linear"], ["zoom"], 4, 1.7, 12, 6]
  }
}

名稱是讀取 name 欄位的符號圖層(symbol layer),文字背後可加上光暈(halo)或不加。缺少字形時不會報錯,因此你指定的字型堆疊必須是這台主機提供的兩組之一:

{
  "id": "road-label-major",
  "type": "symbol",
  "source": "protomaps",
  "source-layer": "roads",
  "minzoom": 11,
  "layout": {
    "text-field": ["coalesce", ["get", "name:en"], ["get", "name"]],
    "text-font": ["Noto Sans Medium"],  // or Noto Sans Regular
    "text-size": 10,
    "symbol-placement": "line"       // follow the road
  },
  "paint": {
    "text-color": "#2b2a26",
    "text-halo-color": "#ffffff",
    "text-halo-width": 1.1
  }
}

圖層沒有繪製任何內容並不算錯誤,MapLibre 也不會告訴你是哪個條件不符。如果畫面一片空白,請依序檢查:欄位名稱、圖層的 minzoom、字型堆疊,最後對照圖示集本身的 light.json 檢查圖示名稱。

如果你的樣式原本將字形(glyphs)指向 demotiles.maplibre.org,只需替換該主機名稱即可。兩者的路徑結構特意設計成完全相同。

"glyphs": "https://tiles.jasontally.com/font/{fontstack}/{range}.pbf",
"sprite": "https://tiles.jasontally.com/sprites/v4/light"

// before, in a style you already have
- "glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
+ "glyphs": "https://tiles.jasontally.com/font/{fontstack}/{range}.pbf",

如果樣式要求的字型堆疊不是上述兩組之一,就不會繪製任何文字。Protomaps 官方樣式使用 Noto Sans Medium 作為粗體文字;這組字型中沒有 Noto Sans Bold,因此要求該字型的樣式會無聲無息地遺失所有標籤。

哪台主機比較快,以及最終的取捨。以實測代替猜測:在單一連線上交錯發送請求,每次一個,捨棄前三筆樣本,並將首位元組時間(TTFB)與傳輸時間分開計算。在該測試中,CDN 對上述每個檔案的回應時間都快了 10 到 45 毫秒,差距明顯超過 15% 的經驗法則。因此,函式庫預設從 jsDelivr 載入。

儘管同一份實測顯示 CDN 回應字型與圖示集的速度約快 10 毫秒,本站仍直接提供這些檔案。這是架構上的取捨,而非數據問題。來自單一主機名稱的地圖不會因為外部服務故障而中斷,一旦字形伺服器當機,整張地圖就會失去所有文字標籤。如果你更在意那 10 毫秒,可以將 glyphs 指向 protomaps.github.io/basemaps-assets/fonts,相同路徑即可運作。以上數據僅為單一城市、單一讀者的測試結果,只需一行指令即可重新執行比較。

五、關於 Leaflet

Leaflet 的 L.tileLayer 用於繪製圖片,而這裡提供的是向量圖磚。Leaflet 地圖依然可以使用它們,而且無論哪種做法都能保留原有的標記、彈出視窗與控制項。兩種替換方式皆說明於從 OpenStreetMap 圖磚遷移。

六、使用 PMTiles 函式庫讀取封存檔

不繪製地圖,只讀取資料。適合用於指令碼、測試,以及任何不需要畫面呈現的場合。

import { PMTiles, FetchSource } from "pmtiles";

const archive = new PMTiles(new FetchSource("https://tiles.jasontally.com/basemap.pmtiles"));

const header = await archive.getHeader();
console.log(header.minZoom, header.maxZoom);        // 0 15
console.log(header.numAddressedTiles);              // 1431655765

const metadata = await archive.getMetadata();
console.log(metadata.name);                          // "Protomaps Basemap"

const tile = await archive.getZxy(3, 4, 2);          // z, x, y
console.log(tile.data.byteLength);

請注意這兩個類別。FetchSource 是透過 HTTP 讀取位元組的讀取器,而 PMTiles 則是建構於其上、負責解析檔案格式的讀取器。FileSource 是對應本機檔案的 FetchSource;若傳入 URL 字串將無法運作。

此封存檔是資料快照,而非即時資料流。更新時程請見資料有多新?。

從 OpenStreetMap 圖磚遷移

圖磚使用政策封鎖、403 錯誤,以及解決方案。免金鑰、無配額限制。

為什麼地圖會顯示封鎖畫面

App is not following the tile usage policy of OpenStreetMap’s volunteer-run servers

General block: app is not following the tile usage policy of openstreetmap volunteer-run servers

403 Forbidden — tile.openstreetmap.org

圖磚伺服器是依據圖磚使用政策,刻意傳送該圖片來代替地圖。這不是系統故障,修改 URL 也無法解除封鎖。

多數人最先嘗試的做法:

辦公室、學校、VPN 與行動網路會共用 IP 位址,而封鎖既會針對應用程式,也會針對位址。從用戶端無法判斷你遇到的是哪一種。

因此,請停止向那些伺服器發送請求。以下提供三種遷移方案。

保留 Leaflet

protomaps-leaflet 可在 Leaflet 中繪製向量圖磚。它會自行讀取封存檔並作為一般圖層加入,因此標記、彈出視窗、GeoJSON 與控制項都能維持原狀。本站提供已針對此封存檔測試過的檔案副本:https://tiles.jasontally.com/vendor/protomaps-leaflet.js。

修改前:

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

<script>
  const map = L.map("map").setView([48.85, 2.35], 12);

  L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
    maxZoom: 19,
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  }).addTo(map);
</script>

修改後:

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
+ <script src="https://tiles.jasontally.com/vendor/protomaps-leaflet.js"></script>

<script>
  const map = L.map("map").setView([48.85, 2.35], 12);

-  L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { ... }).addTo(map);
+  protomapsL.leafletLayer({
+    url: "https://tiles.jasontally.com/basemap.pmtiles",
+    flavor: "light",
+    lang: "en"
+  }).addTo(map);
</script>

上游的 protomaps-leaflet 已進入維護模式:僅修補錯誤,不再新增功能。這對現有的 Leaflet 應用程式來說已經足夠;新專案則建議直接採用 MapLibre GL。

保留 Leaflet,使用 MapLibre 轉譯

@maplibre/maplibre-gl-leaflet 會將 MapLibre 地圖放進 Leaflet 的圖磚窗格(tile pane)中。如果你想在維持使用 Leaflet 的應用程式裡套用上方樣式,或使用地形與山體陰影(hillshade),請選擇此方案。

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="https://tiles.jasontally.com/vendor/maplibre-gl.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://tiles.jasontally.com/vendor/maplibre-gl.js"></script>
<script src="https://tiles.jasontally.com/vendor/pmtiles.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-leaflet@0.1.4/dist/leaflet-maplibre-gl.js"></script>

<script>
  // The style's source is pmtiles://, so MapLibre needs the protocol first.
  maplibregl.addProtocol("pmtiles", new pmtiles.Protocol().tile);

  const map = L.map("map", { minZoom: 1 }).setView([48.85, 2.35], 12);

  const gl = L.maplibreGL({
    style: "https://tiles.jasontally.com/styles/light.json"
  }).addTo(map);

  // gl.getMaplibreMap() is the MapLibre map, for sources and layers.
</script>

遷移至 MapLibre GL

只需一個函式庫、無須轉接器,上方的樣式可直接套用。完整網頁範例請見一、在網頁中嵌入地圖。轉換時主要是對調座標順序:

LeafletMapLibre GL
L.map("map").setView([lat, lng], z)new maplibregl.Map({ container: "map", center: [lng, lat], zoom: z })
L.tileLayer(url).addTo(map)包含向量資料來源的樣式;見上方說明
L.marker([lat, lng]).bindPopup(html)new maplibregl.Marker().setLngLat([lng, lat]).setPopup(new maplibregl.Popup().setHTML(html))
L.geoJSON(data)先呼叫 addSource(id, { type: "geojson", data }),再呼叫 addLayer
L.control.scale()new maplibregl.ScaleControl()
map.invalidateSize()map.resize()

setView([lat, lng]) 先傳入緯度;所有 MapLibre 呼叫則是先經度後緯度 [lng, lat]。如果地圖跑到錯誤的半球,是因為座標順序相反,而非圖磚問題。在樣式載入完成前加入的資料來源與圖層會被直接捨棄且不報錯,因此請在 map.on("load", …) 內加入。

“archive contains MVT vector tiles, but leafletRasterLayer is for displaying raster tiles”

這是因為 pmtiles.leafletRasterLayer 只會將點陣 PMTiles 繪製為圖片。本站提供的是向量圖磚,因此會被拒絕。沒有任何設定選項能改變這一點,本站也沒有提供點陣版本的副本。請改用上方的其中一種方案。

“download.openstreetmap.fr returned 403”

那是資料擷取與同步鏡像站,並非圖磚伺服器。2026 年 10 月,該站因遭受濫用下載而封鎖了許多位址,營運團隊表示僅會依個案恢復存取權限。本站的所有資源都不需要用到它:此封存檔直接來自 Protomaps 的發布版本,你不必自行組裝 .osm.pbf。如果你確實需要下載 OSM 資料,請下載定期的擷取檔(extract),不要拉取每分鐘更新的資料流。

樣式

相同的圖磚,不同的配色。挑選一款,複製並修改,就是你自己的樣式。

樣式外觀預覽檔案

圖磚內容結構

於網頁載入時直接讀取自封存檔本身的中繼資料。樣式必須使用這些圖層名稱。

來源圖層縮放層級欄位
載入中…

本服務不提供的項目

把限制說清楚,避免日後產生非預期的狀況。

資料有多新?

這些圖磚是 Protomaps Basemap 的副本,每月重新建置一次。

樣式 JSON

open raw