Protomaps Basemap 圖磚
完整的 Protomaps Basemap(縮放層級 0 至 15),以單一 PMTiles 封存檔透過標準 HTTP 提供。將任何 PMTiles 用戶端指向該 URL 即可運作。
https://tiles.jasontally.com/basemap.pmtiles
- 免 API 金鑰無須註冊,程式碼中也不必放入權杖。
- 無速率限制不限速、無配額限制、無突發流量上限。
- 每月更新每月根據最新發布的 Protomaps 版本重新建置一次。
遇到 OpenStreetMap 圖磚被封鎖,或收到來自 tile.openstreetmap.org 的 403 錯誤嗎?從這裡開始。
試用
同一個封存檔提供三種樣式。切換樣式時,已載入的圖磚會保留在原處,因此能瞬間完成切換。
自訂樣式
修改顏色或關閉圖層時,地圖會即時更新。接著複製樣式 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\">© 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(只記錄路線編號有幾個字元,而非編號內容),若據此繪製盾牌,每條高速公路上都只會出現空白圖案。
自行撰寫圖層與設定顏色
一個圖層由篩選條件、縮放層級、幾何形狀與顏色組成。篩選條件中有兩個常見陷阱,而且出錯時都不會顯示錯誤訊息:
kind是粗略分類名稱。以道路來說,它的值為highway、major_road、minor_road、path或rail。上游文件中的名稱——motorway、primary、residential——其實是kind_detail的值;如果圖層拿這些值去篩選kind,將不會繪製任何內容,也不會給出任何提示。- 低於圖層自身的
minzoom時不會進行繪製。此封存檔最高只到 z15,因此 minzoom 設為 16 的圖層永遠不會被繪製。大多數圖徵也帶有各自的min_zoom,代表資料建議開始顯示該圖徵的縮放層級,本站提供的樣式同樣會檢查該屬性。
以下範例包含一條道路、路名,以及關閉線條的開關:
{
"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 也無法解除封鎖。
多數人最先嘗試的做法:
- 將
a/b/c子網域改為標準主機名稱並不會解除封鎖。雖然規範要求使用標準 URL,但封鎖是針對應用程式或伺服器收到的 Referer,而非主機名稱。 - 重複重試只會讓情況更糟。請求量過大正是觸發封鎖的原因之一。
- 從本機磁碟開啟的網頁無法靠修改標頭過關。
file://不會傳送Referer。若改傳送X-Requested-With,每個圖磚請求都會觸發 CORS 預檢請求,而圖磚伺服器同樣會拒絕該預檢請求。Firefox 與 Chromium 傳送的標頭雖有差異,但兩者適用的規則相同。 - 在
localhost可以運作,上線後卻被封鎖?問題出在來源網域(origin)。被封鎖的是你正式網域的 Referer,而localhost傳送的 Referer 與它不同。
辦公室、學校、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: '© <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>
flavor可設為light、dark、white、grayscale或black。這些是該函式庫內建的主題,而非上方的樣式,因此顏色不會完全一致。- 標籤使用系統字型直接繪製於 canvas 上。不需要字形 URL,也不會有字型載入 404 的問題。
- 最細的圖磚層級為 z15。超過此層級時圖層會自動放大顯示(overzoom),且預設已啟用
maxDataZoom: 15。
上游的 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>
- 請先註冊通訊協定。若少了
addProtocol那一行,主控台會顯示URL scheme "pmtiles" is not supported,且地圖會保持空白。 - MapLibre 繪製的縮放層級比 Leaflet 低一級。它的圖磚為 512 px,而 Leaflet 為 256 px,因此
minzoom設為 12 的圖層會在 Leaflet 達到 z13 時出現。 - Leaflet 依然在上層進行繪製。Canvas 位於圖磚窗格內,因此標記與疊加層的行為與在點陣圖層上完全相同。
遷移至 MapLibre GL
只需一個函式庫、無須轉接器,上方的樣式可直接套用。完整網頁範例請見一、在網頁中嵌入地圖。轉換時主要是對調座標順序:
| Leaflet | MapLibre 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),不要拉取每分鐘更新的資料流。
樣式
相同的圖磚,不同的配色。挑選一款,複製並修改,就是你自己的樣式。
| 樣式 | 外觀預覽 | 檔案 |
|---|
圖磚內容結構
於網頁載入時直接讀取自封存檔本身的中繼資料。樣式必須使用這些圖層名稱。
| 來源圖層 | 縮放層級 | 欄位 |
|---|---|---|
| 載入中… | ||
本服務不提供的項目
把限制說清楚,避免日後產生非預期的狀況。
- 無服務品質保證(SLA)。這是沒有合約保障的免費服務。它可能會變慢、可能會中斷,且 URL 與資料隨時可能變更或消失而不另行通知。如果你的產品依賴此服務,請自行託管圖磚副本,或向其他廠商購買有支援服務的方案。
- 資料不會每小時即時更新。此副本是最新 Protomaps 發布版本的快照,而非即時資料流。今天在 OpenStreetMap 上所做的編輯不會出現在裡面。更新時程請見資料有多新?。
- 不支援完整檔案下載。伺服器每次僅回應最大 512 KiB 的範圍請求,這已足以應付 PMTiles 用戶端的所有需求。若在單一請求中要求下載整個封存檔,會回傳
416,這是正常現象。 - 必須保留來源標示。這些圖磚衍生自 OpenStreetMap。請在你的樣式或地圖畫面上保留來源標示(attribution)。
資料有多新?
這些圖磚是 Protomaps Basemap 的副本,每月重新建置一次。
- 每月重新建置一次。建置作業於每月第一個星期日的 00:30 至 07:00 UTC 之間執行,這是過去三年發布紀錄中,上游唯一不曾發布新版本的時段。選擇每月而非每週更新,是因為新的封存檔會使 CDN 及所有瀏覽器中已快取的片段失效,而且每次重建都會消耗建置時間。
- 無新版本時不重複建置。只有在上游發布新版本時,才會執行重建。
- 目前使用的是哪一個上游版本?請點擊本頁頂端的封存檔中繼資料按鈕。它會直接讀取封存檔本身的標頭,因此顯示的資訊永遠是最新的,不會因為網頁內容未更新而過時。
- 重建並非瞬間完成。整個過程分為十三個獨立的重建步驟,依序執行,共需約六個半小時。在這段時間的大部分期間,副本會處於新舊兩個上游版本混合的狀態。此時你請求的圖磚可能來自較新的版本,而非讀取器標頭所宣稱的版本,畫面上會顯示為位置錯置的圖磚,但地圖不會損壞。
- 重建可能會跳過。這是盡力而為(best-effort)的排程作業,並非保證成功。如果其中一個步驟失敗,其餘部分會維持在前一個上游版本,地圖依然可以正常運作。