Protomaps Basemap 瓦片

完整的 Protomaps Basemap(缩放级别 0 至 15),通过普通 HTTP 以单个 PMTiles 归档文件提供服务。将任何 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 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,而不包含具体文字,因此据此绘制的盾牌标在所有高速公路上都会显示为空白图形。

自行编写图层并设置颜色

一个图层由过滤条件、缩放级别、几何形状和颜色组成。过滤条件中有两个极易出错的地方,而且出错时都不会报错:

因此,绘制一条道路、道路名称以及控制线条显隐的开关代码如下:

{
  "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 也无法解除封禁。

人们通常会先尝试以下做法:

办公室、学校、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: '&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 的同时使用上方的样式,或者需要地形和山体阴影效果,可以采用此方案。

<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 数据,请下载定期导出包,而不要拉取分钟级增量流。

样式

相同的瓦片,不同的配色。任选其一,复制并修改,即归您所有。

样式外观预览文件

瓦片中包含的内容

页面加载时从归档自身的元数据中读取。样式必须使用以下图层名称。

源图层缩放级别字段
加载中…

服务局限与说明

把话说明白,以免日后出现意外。

数据更新频率

这些瓦片是 Protomaps Basemap 的副本,每月重新构建一次。

样式 JSON

open raw