Skip to content

Latest commit

 

History

History
450 lines (339 loc) · 23.1 KB

api.md

File metadata and controls

450 lines (339 loc) · 23.1 KB

PyMapWidget 接口文档

中文 | English

面向集成方的公共 API。实现细节见 implementation.md。

1. 坐标系简介

1.1 Web Mercator 是什么

Web Mercator(又称 Spherical Mercator、EPSG:3857)是如今网页地图最常用的投影方式,Google 地图、OpenStreetMap、高德/腾讯等在线瓦片服务大体都按这套规则切图。

可以把它理解成两步:

  1. 地理坐标(经纬度):人们熟悉的 (lat, lon),单位是度。具体对应哪个地面点取决于坐标参考系(如 WGS84 或 GCJ-02;见 coordinate_systems.md)。
  2. 投影成平面:在坐标语义与底图一致的前提下,把经纬度「摊」成一张平面图,再按缩放级别切成许多 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)。

1.2 经纬度范围约定

本库同时使用两套经度语义,调用时请按接口说明选用:

名称 纬度 经度 用途
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)

1.3 视口与世界边界

视口始终限制在当前缩放级别的 Web Mercator 世界像素范围 [0, tile_size×2^z) 内(X/Y 独立):

  • 世界某轴 < 视口:该轴地图居中,该轴平移无效;四周空白为控件背景色。
  • 世界某轴 ≥ 视口:可平移,但不能越过该轴世界边缘(硬停)。
  • 不再在拖拽时通过经度环绕拼接「另一侧世界」;瓦片放置也不再放置 wrap 后的重复列。

lat_lon_to_pixel 仍接受 Continuous 经度;对外展示接口继续返回 Canonical。


2. 配置:MapConfig

模块: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,后一来源会命中前一来源相同坐标的已有瓦片,造成底图混用。切换来源时应同时切换缓存目录,或由应用明确清空原缓存。


3. 地图控件(Backend)

PyQtMapWidget、PyQt6MapWidget、PySide6MapWidget 与 TkMapWidget 对外接口对齐。下列签名以控件实例为 map_widget。

3.1 构造

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 异步瓦片就绪后合并刷新视图的间隔(毫秒)

3.2 视图与状态

方法 返回 说明
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。

3.3 坐标换算

方法 参数 / 返回 经纬度范围
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)

3.4 缩放与叠加控件

默认显示:右下角 定位 ◎ + + / −,左下角 比例尺(按视口中心纬度实时计算;文字居中于刻度线)。

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 距离)。

3.5 矢量叠加(行政区 / POI)

数据模型: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。

3.6 配置与调试

方法 说明
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] 同时导出上述两者

3.7 生命周期

后端 关闭
PyQt5 窗口关闭走 closeEvent;也可依赖父控件销毁
PyQt6 窗口关闭走 closeEvent;也可依赖父控件销毁
PySide6 窗口关闭走 closeEvent;也可依赖父控件销毁
Tkinter 调用 destroy()(会 controller.close())

3.8 PySide6 MapLibre 矢量地图控件

模块: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.py

Demo 的「显示瓦片分界线」「显示南京行政区 + POI」「行政区域高亮」开关分别演示上述 API;POI 在 zoom 9 以上显示,悬停时显示名称和描述。

默认 MapLibre JS/CSS 从 unpkg 加载。需要完全离线或固定前端版本时,应用应自行托管这些资源并传入对应 URL。Qt WebEngine 使用 Chromium 网络栈和 HTTP cache,不写入本项目栅格 tiles.db。


4. 核心控制器:MapController

模块:pymapwidget.core.map_controller.MapController 一般由 Backend 创建;无 UI 场景可直接使用。

4.1 构造要点

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 主线程执行

4.2 视口操作

方法 说明
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() 关闭下载器与缓存连接

4.3 查询

方法 返回范围 / 说明
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() 统计

5. 坐标核心:mercator(常用)

模块: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

6. 图源 CRS(GCJ-02 / WGS84)— 调用约定

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。


7. 使用示例

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)

8. 相关文档

文档 内容
README.md 安装、Demo、简要示例
coordinate_systems.md WGS84、GCJ-02、转换原理与常见错误
pbf_basics.md PBF/MVT、Style 与 MapLibre 入门
implementation.md 架构、缓存、加载/重绘、矢量叠加与图源 CRS 数据流
AGENTS.md Agent 协作约束