中文 | English
面向集成方的公共 API。实现细节见 implementation.md。
Web Mercator(又称 Spherical Mercator、EPSG:3857)是如今网页地图最常用的投影方式,Google 地图、OpenStreetMap、高德/腾讯等在线瓦片服务大体都按这套规则切图。
可以把它理解成两步:
- 地理坐标(经纬度):人们熟悉的
(lat, lon),单位是度。具体对应哪个地面点取决于坐标参考系(如 WGS84 或 GCJ-02;见 coordinate_systems.md)。 - 投影成平面:在坐标语义与底图一致的前提下,把经纬度「摊」成一张平面图,再按缩放级别切成许多 256×256 的正方形图片,也就是常说的 slippy map 瓦片。每个瓦片用整数坐标
(z, x, y)标识:z:缩放级别(越大越详细);x:从西向东的列号;y:从北向南的行号。
本库的 mercator 模块负责:经纬度 ↔ 世界像素 ↔ 瓦片索引之间的换算(Web Mercator 投影;不负责 WGS84 / GCJ-02 互转),从而知道「屏幕上某一点对应哪块瓦片、该去哪个 URL 下载」。
和真正椭球墨卡托相比,Web Mercator 把地球当成正球体来算,实现简单、与主流瓦片服务一致。代价是高纬度变形更大,且无法表示两极附近:
- 有效纬度大约在 ±85.05°(本库常量
MAX_LAT = 85.05112878); - 再往北/南的点会被夹到这个范围内,否则投影公式会发散。
经度本身可以绕地球转圈;显示时折回 [-180°, 180°)。内部状态仍可用 Continuous 经度做像素换算;可见视口受世界边界约束,拖拽不会环绕接上另一侧(见 §1.3)。
本库同时使用两套经度语义,调用时请按接口说明选用:
| 名称 | 纬度 | 经度 | 用途 |
|---|---|---|---|
| Canonical(常规 / 显示) | [-85.05112878, 85.05112878](Web Mercator 有效带) |
[-180, 180) |
UI 展示、配置默认中心、瓦片索引 |
| Continuous(连续 / 世界) | 同上 | 可超出 [-180, 180)(如 -241) |
内部平移、世界像素连续换算 |
- 显示与对外「当前中心 / 鼠标位置」接口返回 Canonical。
lat_lon_at_screen等底层反算可返回 Continuous;需要展示时再调用normalize_lat_lon。- 算瓦片
x/y时,经度会先 wrap 到 Canonical,再索引。
辅助函数(pymapwidget.core.mercator):
| 函数 | 说明 |
|---|---|
clamp_lat(lat) |
纬度夹到 Web Mercator 有效范围 |
wrap_lon(lon) |
经度折进 [-180, 180) |
normalize_lat_lon(lat, lon) |
返回 Canonical (lat, lon) |
视口始终限制在当前缩放级别的 Web Mercator 世界像素范围 [0, tile_size×2^z) 内(X/Y 独立):
- 世界某轴 < 视口:该轴地图居中,该轴平移无效;四周空白为控件背景色。
- 世界某轴 ≥ 视口:可平移,但不能越过该轴世界边缘(硬停)。
- 不再在拖拽时通过经度环绕拼接「另一侧世界」;瓦片放置也不再放置 wrap 后的重复列。
lat_lon_to_pixel 仍接受 Continuous 经度;对外展示接口继续返回 Canonical。
模块:pymapwidget.core.config.MapConfig
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
cache_dir |
Path |
(必填) | 瓦片缓存根目录;不校验瓦片来源,不同来源必须使用不同目录 |
tile_url_templates |
list[str] |
Google 示例 | URL 模板,支持 {x} {y} {z} 与 {quadkey},不可为空 |
proxy |
str | None |
None |
HTTP(S) 代理 |
default_zoom |
int |
15 |
初始缩放 |
default_center |
tuple[float, float] |
北京附近 | 推荐 Canonical (lat, lon) |
max_concurrency |
int |
8 |
下载并发 |
min_zoom / max_zoom |
int |
3 / 18 |
缩放范围 |
tile_size |
int |
256 |
瓦片边长(像素) |
bucket_size |
int |
256 |
缓存目录桶大小 |
user_agent |
str |
浏览器 UA | HTTP User-Agent;OSM 官方瓦片须用可识别应用名(见 implementation.md §4.1) |
reject_blocked_tiles |
bool |
False |
是否按 blocked_tile_md5 拒绝已知拦截图(默认关,按图源打开) |
blocked_tile_md5 |
list[str] |
[] |
整图 MD5(32 位小写 hex);仅当 reject_blocked_tiles=True 时生效 |
request_timeout |
float |
10.0 |
请求超时(秒) |
max_retries |
int |
2 |
网络失败后额外尝试次数(拦截图命中不走此重试) |
tile_path_cache_size |
int |
1000 |
已解析瓦片路径 LRU 上限;0 表示不限制 |
tile_coord_ref |
str |
"gcj02" |
底图影像坐标系:wgs84 或 gcj02(见 §6) |
构造时会校验 zoom 范围、初始 zoom、并发数、瓦片/桶大小、超时与重试次数;无效值会抛出 ValueError。非 256 的 tile_size 会同时用于坐标换算、视口布局、比例尺和后端图像缩放。
缓存索引主键仅为 (z, x, y),不包含 tile_url_templates。如果两个地图源共用同一个 cache_dir,后一来源会命中前一来源相同坐标的已有瓦片,造成底图混用。切换来源时应同时切换缓存目录,或由应用明确清空原缓存。
PyQtMapWidget、PyQt6MapWidget、PySide6MapWidget 与 TkMapWidget 对外接口对齐。下列签名以控件实例为 map_widget。
PyQt5
from pymapwidget.backends.pyqt5 import PyQtMapWidget
map_widget = PyQtMapWidget(
config,
*,
show_status: bool = True,
view_blit_coalesce_ms: int = 25,
parent: QWidget | None = None,
)PyQt6 / PySide6
from pymapwidget.backends.pyqt6 import PyQt6MapWidget
from pymapwidget.backends.pyside6 import PySide6MapWidget
pyqt6_widget = PyQt6MapWidget(config, show_status=True, parent=None)
pyside6_widget = PySide6MapWidget(config, show_status=True, parent=None)两个 Qt 6 控件的构造参数与 PyQtMapWidget 相同。应用中应选择一种 Qt binding,不要在同一进程混用。
Tkinter
from pymapwidget.backends.tkinter import TkMapWidget
map_widget = TkMapWidget(
master,
config,
*,
show_status: bool = True,
view_blit_coalesce_ms: int = 25,
)| 参数 | 说明 |
|---|---|
config |
MapConfig |
show_status |
是否在控件底部显示内置状态条(demo 一般传 False,改用左侧面板) |
view_blit_coalesce_ms |
异步瓦片就绪后合并刷新视图的间隔(毫秒) |
| 方法 | 返回 | 说明 |
|---|---|---|
get_center() |
tuple[float, float] |
视口中心 Canonical (lat, lon),含当前平移 |
get_zoom() |
int |
当前缩放级别 |
get_view_stats() |
MapViewStats |
中心、四角、可见/已加载瓦片数;经纬度均为 Canonical |
get_pointer_info() |
MapPointerInfo | None |
鼠标在地图上时:屏幕坐标、Canonical 经纬度、首条瓦片 URL;移出地图为 None |
MapViewStats 字段:zoom, center_lat, center_lon, corners(nw/ne/sw/se → (lat, lon)), visible_tile_count, loaded_tile_count。
MapPointerInfo 字段:screen_x, screen_y, lat, lon, tile_url。
| 方法 | 参数 / 返回 | 经纬度范围 |
|---|---|---|
lat_lon_to_tile_xy(lat, lon, zoom) |
→ (tile_x, tile_y) |
入参 lon 可为 Continuous,内部 wrap 后索引 |
lat_lon_at_tile_pixel(tile_x, tile_y, zoom, pixel_x, pixel_y) |
→ (lat, lon) |
常规瓦片索引下 lon ∈ [-180, 180) |
默认显示:右下角 定位 ◎ + + / −,左下角 比例尺(按视口中心纬度实时计算;文字居中于刻度线)。
ControlCorner(pymapwidget.core.map_controls):top_left / top_right / bottom_left / bottom_right。
| 方法 | 说明 |
|---|---|
set_center(lat, lon) |
设置视口中心 |
set_zoom(zoom) |
设置缩放(夹到当前范围),以视口中心为锚 |
set_zoom_range(min_zoom, max_zoom) / get_zoom_range() |
运行时缩放范围,默认 [3, 18] |
set_on_locate(callback | None) |
点击定位按钮时回调;库本身不取 GPS/IP,由应用决定如何定位并调用 set_center |
set_zoom_control_visible(bool) / zoom_control_visible() |
缩放/定位控件显隐(默认显示) |
set_zoom_control_position(corner) / zoom_control_position() |
缩放/定位控件四角位置(默认 bottom_right) |
set_scale_bar_visible(bool) / scale_bar_visible() |
比例尺显隐(默认显示) |
set_scale_bar_position(corner) / scale_bar_position() |
比例尺四角位置(默认 bottom_left) |
set_scale_bar_distances({zoom: meters}) / scale_bar_distances() |
某 zoom 强制使用给定米数;未配置的 level 自动选 nice 距离 |
比例尺算法见 pymapwidget.core.scale_bar.compute_scale_bar(Web Mercator m/px + Leaflet 风格 nice 距离)。
数据模型:pymapwidget.core.map_overlay 的 MapPolygon、MapPoi、CoordRef。
行政区 / POI 是 矢量叠加(经纬度 → 路径/点图元),不是画进瓦片缓存的栅格,也不是对每块瓦片判断「是否在某市内」再染色。底图仍是 PNG 瓦片;叠加层盖在上面画折线 / 点。
分层模型与带坐标示例见 implementation.md §9;下表为控件 API。
实现状态
| 后端 | 行为 |
|---|---|
| PyQt5 / PyQt6 / PySide6 | 完整:透明 QGraphicsView 叠在瓦片层上;Scene 使用当前 zoom 的世界像素;与视口对齐显示 |
| Tkinter | 不提供矢量叠加 API |
| 方法 | 说明 |
|---|---|
set_tile_coord_ref(ref) / tile_coord_ref() |
底图影像 CRS(wgs84 / gcj02) |
set_polygons(polygons) |
设置行政区等多边形(描边;填充受 set_polygon_fill_enabled 控制) |
set_pois(pois) |
设置 POI;悬停显示气泡 tip(主体在标记正上方、下缘不盖住 POI,底部三角指向标记;贴顶时改到下方) |
set_vector_overlays(polygons, pois) |
一次替换多边形与 POI(只重建一次叠加层) |
clear_vector_overlays() |
清空多边形与 POI |
set_polygon_fill_enabled(bool) / polygon_fill_enabled() |
半透明区域高亮开关(关闭后仍保留分界线) |
fit_bounds(min_lat, min_lon, max_lat, max_lon, *, coord_ref=None, padding_px=40) |
按范围适配视口;coord_ref 与底图不一致时自动转换 |
fit_overlay_bounds(bounds, *, padding_px=40) |
对 OverlayBounds 适配视口 |
多边形 / POI 的 coord_ref 表示数据自身的坐标系;绘制前会转换到 tile_coord_ref 再投影。详见 §6;算法与 Demo 行为见 implementation.md §10。
| 方法 | 说明 |
|---|---|
apply_config(config: MapConfig) |
热重载配置(关闭旧下载器并废弃旧回调,按新中心/zoom/URL 重建;切换来源时调用方须同时更换/清理缓存目录) |
set_show_tile_borders(show: bool) |
是否在离屏缓冲上绘制瓦片网格线 |
show_tile_borders() -> bool |
当前是否显示网格线 |
view_blit_coalesce_ms() -> int |
读取合并 blit 间隔 |
set_view_blit_coalesce_ms(ms: int) |
设置合并 blit 间隔 |
save_map_view(path="map.png") -> Path |
导出当前视口(瓦片 + 矢量叠加) |
save_back_buffer(path="buffer.png") -> Path |
导出含缓冲圈的离屏大缓冲 |
save_debug_images(map_path, buffer_path) -> tuple[Path, Path] |
同时导出上述两者 |
| 后端 | 关闭 |
|---|---|
| PyQt5 | 窗口关闭走 closeEvent;也可依赖父控件销毁 |
| PyQt6 | 窗口关闭走 closeEvent;也可依赖父控件销毁 |
| PySide6 | 窗口关闭走 closeEvent;也可依赖父控件销毁 |
| Tkinter | 调用 destroy()(会 controller.close()) |
模块:pymapwidget.maplibre。这是独立的 Qt WebEngine + MapLibre GL JS 控件,不使用 MapConfig、MapController、栅格缓存或栅格后端。它加载的是 MapLibre Style JSON;Style 引用的 MVT/PBF、glyph 和 sprite 由 MapLibre 下载与渲染。
from pymapwidget.maplibre import PySide6MapLibreWidget
map_widget = PySide6MapLibreWidget(
"https://tiles.openfreemap.org/styles/liberty",
center=(32.05, 118.78), # (lat, lon), WGS84
zoom=10.0, # 连续浮点 zoom
min_zoom=0.0,
max_zoom=24.0,
)构造参数:
| 参数 | 类型 / 默认 | 说明 |
|---|---|---|
style |
str | dict,必填 |
Style JSON URL 或已解析的 Style 字典 |
center |
(0.0, 0.0) |
WGS84 (lat, lon);传入经度会 wrap 到 [-180, 180) |
zoom |
1.0 |
连续缩放级别,夹到 zoom 范围 |
min_zoom / max_zoom |
0.0 / 24.0 |
连续缩放范围 |
bearing / pitch |
0.0 / 0.0 |
初始旋转和倾斜;pitch 范围 [0, 85] |
base_url |
https://pymapwidget.local/ |
字典 Style 内相对 URL 的解析基址 |
maplibre_js_url / maplibre_css_url |
unpkg 的 MapLibre GL JS 5.x | 可换为应用自行托管或本地 scheme 的资源 |
parent |
None |
Qt 父控件 |
视图 API:
| 方法 | 说明 |
|---|---|
is_ready() |
MapLibre 首次触发 load 后为 True |
get_center() / set_center(lat, lon) |
读取/设置 WGS84 (lat, lon) |
get_zoom() / set_zoom(zoom) |
读取/设置连续浮点 zoom |
get_zoom_range() / set_zoom_range(min, max) |
连续 zoom 范围 |
get_bounds() |
最近视图的 (west, south, east, north);首次状态到达前为 None |
get_view_state() |
返回 MapLibreViewState |
get_pointer_info() |
返回最近的 MapLibrePointerInfo;鼠标尚未进入或已经移出地图时为 None |
fit_bounds(min_lat, min_lon, max_lat, max_lon, *, coord_ref="wgs84", padding_px=40, max_zoom=None) |
适配范围;输入可声明 WGS84 / GCJ-02,WGS84 经度原样传递以允许跨日期线范围 |
style() / set_style(style) |
读取/切换 Style URL 或字典 |
调试与叠加层 API:
| 方法 | 说明 |
|---|---|
set_show_tile_borders(show) / show_tile_borders() |
设置/读取 MapLibre 原生瓦片调试边界 |
set_polygons(polygons) / set_pois(pois) |
设置共享 MapPolygon / MapPoi 数据模型中的一类叠加物 |
set_vector_overlays(polygons, pois) |
一次更新行政区和 POI;Python 转为 WGS84 GeoJSON 后交给 WebGL 渲染 |
clear_vector_overlays() |
移除 GeoJSON source、行政区 fill/line layer 和 POI circle layer |
set_polygon_fill_enabled(enabled) / polygon_fill_enabled() |
控制行政区填充;描边保持显示 |
fit_overlay_bounds(bounds, *, padding_px=40) |
按 OverlayBounds.coord_ref 转换到 WGS84 后适配视口 |
叠加层使用 pymapwidget.core.map_overlay 中与栅格 Qt 后端相同的 MapPolygon、MapPoi 和 OverlayBounds。MapLibre 固定按 WGS84 接收 GeoJSON;GCJ-02 数据(包括 Demo 南京行政区)在 Python 侧转换。切换 Style 后,控件会在 style.load 时自动恢复叠加层与瓦片边界状态。
回调 API:
| 方法 | 回调参数 |
|---|---|
set_on_ready(callback) |
无参数;首次 load 后调用一次 |
set_on_map_changed(callback) |
MapLibreViewState;首次 load 以及每次 moveend / resize 调用 |
set_on_pointer_changed(callback) |
MapLibrePointerInfo | None;网页端鼠标移动时推送,移出地图时推送 None |
set_on_feature_clicked(callback) |
MapLibreClickEvent;包含点击经纬度、屏幕坐标和最多 100 个 rendered feature 摘要 |
set_on_error(callback) |
错误字符串;包括 Style、瓦片、glyph、sprite 和桥接错误 |
控件也提供对应 Qt signals:ready、view_changed(object)、pointer_changed(object)、feature_clicked(object)、error(str)。
MapLibreViewState 字段:center_lat, center_lon, zoom, bearing, pitch, bounds, corners;corners 是 nw/ne/sw/se 到 (lat, lon) 的映射,并且在倾斜或旋转时仍对应真实屏幕四角。MapLibrePointerInfo 字段:screen_x, screen_y, lat, lon, tile_url, source_id, tile_z, tile_x, tile_y。鼠标瓦片取 Style 中首个可用 vector source,在 source 的 minzoom/maxzoom 范围内计算;无可用 URL 模板时 tile_url 为空。MapLibreClickEvent 字段:lat, lon, screen_x, screen_y, features。每个 feature 摘要包含 id、source、sourceLayer、layerId、layerType、geometryType、properties,不回传可能很大的完整 geometry。
pip install -e ".[maplibre-pyside6]"
python demos/demo_maplibre_pyside6.pyDemo 的「显示瓦片分界线」「显示南京行政区 + POI」「行政区域高亮」开关分别演示上述 API;POI 在 zoom 9 以上显示,悬停时显示名称和描述。
默认 MapLibre JS/CSS 从 unpkg 加载。需要完全离线或固定前端版本时,应用应自行托管这些资源并传入对应 URL。Qt WebEngine 使用 Chromium 网络栈和 HTTP cache,不写入本项目栅格 tiles.db。
模块:pymapwidget.core.map_controller.MapController
一般由 Backend 创建;无 UI 场景可直接使用。
MapController(
config: MapConfig,
*,
width: int = 800,
height: int = 600,
on_map_changed: Callable[[], None] | None = None,
on_tile_updated: Callable[[TileKey, Path | None, TilePlacement], None] | None = None,
main_thread_invoker: Callable[[Callable[[], None]], None] | None = None,
)| 回调 | 说明 |
|---|---|
on_map_changed |
视口需全量重绘时(zoom / resize / normalize 后等) |
on_tile_updated |
单瓦片就绪;须经 main_thread_invoker 回到 UI 线程 |
main_thread_invoker |
将闭包投递到 UI 主线程执行 |
| 方法 | 说明 |
|---|---|
set_size(width, height) |
设置像素尺寸并刷新瓦片 |
set_center(lat, lon) |
设置中心;lat 会 clamp;lon 可为 Continuous;无变化则跳过;已加载瓦片会复用 |
set_zoom(zoom) |
设置缩放(夹到 min/max) |
set_zoom_range(min_zoom, max_zoom) / get_zoom_range() |
设置/读取缩放范围并夹紧当前 zoom |
pan_by(dx, dy) |
像素平移(屏幕坐标增量) |
end_pan() |
松手:整瓦片 offset 折入中心;无变化则跳过重绘 |
zoom_at(screen_x, screen_y, delta) |
以屏幕点为锚点缩放(delta 为 ±1 级) |
refresh_tiles() |
视图版本号加一,并重新请求当前视口全部瓦片(旧下载结果作废) |
close() |
关闭下载器与缓存连接 |
| 方法 | 返回范围 / 说明 |
|---|---|
get_view_center() |
Canonical (lat, lon) |
get_view_corners() |
Canonical 四角 |
get_view_stats() |
见 §3.2 |
lat_lon_at_screen(x, y) |
Continuous (lat, lon) |
tile_url_at_screen(x, y) |
首条模板填入当前点瓦片 x/y/z(及 {quadkey},若模板使用) |
lat_lon_to_tile_xy / lat_lon_at_tile_pixel |
同 §3.3 |
viewport_origin_world_pixels() |
视口左上角世界像素 |
fit_bounds(min_lat, min_lon, max_lat, max_lon, *, padding_px=40) |
适配地理范围 |
get_placements() |
当前缓冲圈内 TilePlacement 列表 |
get_tile_path(key) |
已就绪瓦片本地路径,否则 None |
placement_visible_in_viewport(placement) |
是否与视口相交(不含仅缓冲圈) |
count_visible_tiles() / count_loaded_tiles() |
统计 |
模块:pymapwidget.core.mercator
| 函数 | 入参经纬度 | 返回经纬度 |
|---|---|---|
normalize_lat_lon(lat, lon) |
任意 | Canonical |
lat_lon_to_tile(lat, lon, zoom) |
lon 任意(内部 wrap) | — → (x, y) |
tile_to_lat_lon(x, y, zoom) |
— | Canonical(西北角) |
lat_lon_to_pixel(lat, lon, zoom, tile_size=256) |
lon Continuous 保留不 wrap | — → 世界像素 |
pixel_to_lat_lon(px, py, zoom, tile_size=256) |
— | Continuous |
tile_pixel_to_lat_lon(..., tile_size=256) |
— | 见函数 docstring |
WGS84、GCJ-02、Web Mercator 的区别以及正向/逆向转换原理见 WGS84、GCJ-02 与坐标转换入门。本节只定义公共 API 的调用契约:叠加数据先转换到与底图一致的 MapConfig.tile_coord_ref,再进行 Web Mercator 投影。
| API | 说明 |
|---|---|
MapConfig.tile_coord_ref |
底图影像 CRS:wgs84 | gcj02 |
CoordRef / MapPolygon.coord_ref / MapPoi.coord_ref |
叠加数据自身 CRS |
wgs84_to_gcj02 / gcj02_to_wgs84 / convert_lat_lon(core/gcj02) |
点坐标互转 |
map_overlay.convert_point |
叠加层用的封装 |
切换图源时的契约:
- 视口中心 / zoom:库不会自动做 GCJ↔WGS;数字原样使用(Demo 共用
default_center即为此)。 - 矢量叠加:投影前按
tile_coord_ref会转换。 - 若业务要「同一地面点」切换底图,由应用自行调用
convert_lat_lon。
项目内的转换位置与 Demo 图源对照见 implementation.md §10;OSM「Access blocked」配置见 implementation.md §4.1。
from pathlib import Path
from pymapwidget.core.config import MapConfig
from pymapwidget.core.map_overlay import CoordRef, MapPoi, MapPolygon
from pymapwidget.backends.pyqt5 import PyQtMapWidget
config = MapConfig(
cache_dir=Path("./map_cache"),
default_center=(32.06, 118.80),
default_zoom=11,
tile_coord_ref="gcj02",
)
widget = PyQtMapWidget(config, show_status=False)
widget.set_polygons(
[
MapPolygon(
id="demo",
rings=[[(32.05, 118.75), (32.08, 118.75), (32.08, 118.82), (32.05, 118.82)]],
coord_ref=CoordRef.GCJ02,
name="demo",
)
]
)
widget.set_pois(
[MapPoi(id="p1", lat=32.06, lon=118.79, name="示例", coord_ref=CoordRef.GCJ02)]
)
widget.set_polygon_fill_enabled(True)| 文档 | 内容 |
|---|---|
| README.md | 安装、Demo、简要示例 |
| coordinate_systems.md | WGS84、GCJ-02、转换原理与常见错误 |
| pbf_basics.md | PBF/MVT、Style 与 MapLibre 入门 |
| implementation.md | 架构、缓存、加载/重绘、矢量叠加与图源 CRS 数据流 |
| AGENTS.md | Agent 协作约束 |