Protomaps Basemap 瓦片
完整的 Protomaps Basemap(缩放级别 0 至 15),通过普通 HTTP 以单个 PMTiles 归档文件提供服务。将任何 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 schema v3。为其他 schema 编写的样式虽然能加载,但不会绘制任何内容,因为源图层名称不匹配。下方列出了全部九个图层名称,以及可供复制的现成 v3 样式。
四、字体、精灵图与依赖库
一张地图由瓦片、用于文字的 glyphs URL、可选的用于图标的 sprite URL 以及负责绘制的库组成。本站按各自所需的标准路径提供了上述全部资源,因此如有需要,整张地图可以从单一主机名加载。
| 资源 | 本站 URL | 许可证 |
|---|
MapLibre 和 PMTiles 文件同时托管在 jsDelivr 和本站上,使用任一 URL 均可。
本页未使用表格最后一行。它是 Leaflet 矢量渲染器,发布在本站以便替换时只需改动一个脚本标签。Leaflet 本身仍保留在其官方 CDN 上。
包含 Noto Sans Regular 和 Noto Sans Medium 两个字体栈,按 MapLibre 的要求拆分为 256 个区间,每次请求 256 个码位。本站发布了全部 256 个区间,而非仅限拉丁字符:Protomaps 使用当地名称标注地理要素,因此东京或开罗等地需要数万号段的编码区间。如果只发布英文地图所需的字符,这些标注就会消失,且不会报任何错误。
共有五套精灵图(sprite sheet),对应上游的五种配色方案(flavour),均提供 1x 和 2x 分辨率。下方的样式会从中获取并绘制图标。如果您想在自己的样式中显示城镇圆点(townspots)、公路盾牌标或地点图标,请将 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)图层,文字后方可以带光晕(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 检查图标名称。
如果您的样式将字形指向 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 地图仍然可以使用它们,且两种接入方式都能保留现有的标记(Marker)、弹窗(Popup)和控件。两种替换方法均见从 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 和移动网络会共用网络地址,而封禁既针对应用也针对网络地址。从您的角度无法判断属于哪种情况。
因此,请停止向这些服务器发起请求。以下有三种迁移方案。
保留 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 的同时使用上方的样式,或者需要地形和山体阴影效果,可以采用此方案。
<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 像素,而 Leaflet 是 256 像素,因此
minzoom为 12 的图层会在 Leaflet 达到 z13 时显示。 - Leaflet 仍然在上层绘制。画布位于瓦片窗格内,因此标记和覆盖物的行为与在栅格图层上一致。
迁移至 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 数据,请下载定期导出包,而不要拉取分钟级增量流。
样式
相同的瓦片,不同的配色。任选其一,复制并修改,即归您所有。
| 样式 | 外观预览 | 文件 |
|---|
瓦片中包含的内容
页面加载时从归档自身的元数据中读取。样式必须使用以下图层名称。
| 源图层 | 缩放级别 | 字段 |
|---|---|---|
| 加载中… | ||
服务局限与说明
把话说明白,以免日后出现意外。
- 无服务级别保障。这是一项没有合同约束的免费服务。它可能会变慢、可能会宕机,URL 和数据也可能在不经通知的情况下变更或下线。如果您的产品依赖它,请自行托管瓦片副本,或向服务商购买有技术支持的服务方案。
- 数据并非按小时实时更新。此副本是最新 Protomaps 发布版本的快照,而非实时数据流。今天提交的地图编辑不会出现在其中。更新时间详见数据更新频率。
- 不支持整文件下载。每个范围请求最多返回 512 KiB 数据,这已涵盖了 PMTiles 客户端所需的全部大小。如果在单次请求中索取整个归档文件,服务器会按设计返回
416。 - 必须保留数据来源署名。这些瓦片源自 OpenStreetMap。请在您的样式或地图上保留署名信息。
数据更新频率
这些瓦片是 Protomaps Basemap 的副本,每月重新构建一次。
- 每月重新构建一次。构建任务在每月第一个星期日的 00:30 至 07:00(UTC)之间运行——从过去三年的发布记录来看,这是上游唯一未曾发布新版本的时间窗口。之所以按月而非按周更新,是因为新归档会让 CDN 和所有已缓存该数据的浏览器缓存失效,而且每次重新构建都会消耗构建时长。
- 大多数月份无需重新构建。只有当上游发布了新版本时,才会执行重新构建。
- 当前使用的是哪个上游构建版本?请使用本页顶部的归档元数据按钮。它会读取归档文件自身的头部信息,因此显示的始终是当前实际版本,不会因为网页内容过时而产生偏差。
- 重新构建并非瞬间完成。整个过程包含十三个独立的重新构建步骤,依次执行,耗时约六个半小时。在此时间窗口的大部分时间里,线上副本由两个上游构建版本混合而成。在此期间请求的瓦片可能来自较新的构建版本,而非读取器所通告的版本,这会表现为瓦片位置错位,但不会导致地图损坏。
- 重新构建有时可能跳过。这是一项尽力而为的任务,不做保证。如果某一步骤失败,其余部分会保留在上一个上游构建版本,地图依然可以正常使用。