通过 pywebview 桌面容器 + JS Bridge,让 Python 代码直接控制 BICMap JS SDK 进行地图展示与数据渲染,无需编写任何前端代码。
在线文档 & 示例:bicmap.x-humanoid-cloud.com
项目采用三层分离设计,从上到下依次为:视图容器层 → 桥接通信层 → 地图渲染层。
Python 用户代码
│
▼
┌─ BicMapView (视图容器层) ────────────────────────────┐
│ │
│ view = BicMapView(title, width, height) │
│ view.bridge.on_ready = on_ready ← 回调入口 │
│ view.bridge.on_click = on_click │
│ view.start() ← 阻塞启动 │
│ │
│ 职责: WebView 窗口 + HTTP 静态服务器 + 截图 IO │
└────────────────────┬──────────────────────────────────┘
│
│ 双向:invoke() 下行 / pywebview.api 上行
▼
┌─ BicMapClient (桥接通信层) ───────────────────────────┐
│ │
│ bridge.fly_to(center) ← 顶层操作 │
│ bridge.marker.add(lng, lat, label) ← 子命名空间 │
│ bridge.geometry.create_polygons(...) │
│ bridge.drawing.enable_polygon(...) │
│ │ │
│ │ invoke(ns, action, **params) │
│ │ → json.dumps → evaluate_js(code) │
│ ▼ │
│ window.__bicMapBridge.dispatch({ns,action,params}) │
│ │
│ 职责: 参数序列化 → JS 注入 → 回调解析 → 用户通知 │
└────────────────────┬──────────────────────────────────┘
│
│ 上行回调:on_map_ready / on_map_click / on_result
▼
┌─ JS Bridge (JS 执行层) ───────────────────────────────┐
│ │
│ bridge-adapter.mjs │
│ createBridge(bicMap) → 构建 ROUTES 路由表 │
│ "" → createMap, flyTo, screenshot, ... │
│ geometry → createPointCloud, remove, update │
│ marker → add, addDirectional, addRobot │
│ drawing → enablePolygon, enableCircle │
│ layer → addGeoJSONSource, addFill │
│ resource → getAssetPath, getFontPath │
│ slam → load (异步) │
│ │ │
│ ▼ │
│ bic-map-plugin.mjs → bicMap.min.js │
│ (MapLibre GL + Three.js + Turf.js) │
│ │
│ 职责: 路由分发 → SDK 调用 → 事件回传 │
└───────────────────────────────────────────────────────┘
核心调用流程(下行 Python → JS):
view.bridge.marker.add(116.39, 39.90, "北京")
→ _MarkerAPI.add() → client.invoke("marker", "add", lng=116.39, lat=39.90, label="北京")
→ json.dumps({ns:"marker", action:"add", params:{lng,lat,label}})
→ window.evaluate_js("window.__bicMapBridge.dispatch(...)")
→ ROUTES["marker"]["add"](params)
→ new maplibregl.Marker().setLngLat([lng,lat])...
核心回调流程(上行 JS → Python):
map.on('click')
→ pywebview.api.on_map_click(JSON.stringify({lng,lat}))
→ BicMapView.on_map_click(json_str) ← pywebview 路由
→ BicMapClient._on_map_click(json_str)
→ MapClickEvent(lng, lat) → user.on_click(event)
bridge-adapter.mjs 是连接 Python 侧与 bicMap JS SDK 的核心纽带,其设计必要性体现在以下方面:
| 必要性 | 说明 |
|---|---|
| 双向通信路由 | Python 通过 invoke(ns, action, params) 发送结构化消息,adapter 内部的 ROUTES 路由表按命名空间(marker / geometry / drawing / slam 等)统一分发到对应处理函数,Python 无需了解任何 MapLibre / bicMap API 细节 |
| 控制器生命周期管理 | 对于创建型操作(如 geometry.createPolygons),adapter 通过 _registry 记录每个控制器实例的 controllerId,支持后续的 update() / remove() / show() / hide() 操作,避免 Python 侧维护 JS 对象引用 |
| 异步操作同步化 | 对于 slam.load() 等 JS 异步操作,adapter 注入 _asyncId 并通过 pywebview.api.on_result() 回调将结果回传,使得 Python 侧可以用 _invoke_async + Event 实现同步等待 |
| 模板与 SDK 解耦 | HTML 模板只需 import { createBridge } from 'bridge-adapter.mjs',不直接依赖 bicMap SDK 的具体接口。SDK 版本升级、接口变化只需修改 adapter 内部实现,模板和 Python 代码无需变更 |
| 事件反向桥接 | 地图事件(click / load / 绘制完成等)通过 pywebview.api.on_xxx() 回调回传 Python,adapter 负责将 SDK 事件格式规范化为统一的结构化数据后再传输 |
核心设计模式:adapter 将双向通信抽象为 下行路由 + 上行回调 的对称模型。
Python: bridge.marker.add(lng, lat, label)
→ invoke("marker", "add", {lng, lat, label})
→ dispatch({ns:"marker", action:"add", params:{...}})
→ ROUTES["marker"]["add"](params) ← 下行路由
→ new maplibregl.Marker().addTo(_map)
JS map.on('click', (e) => {
→ pywebview.api.on_map_click(JSON.stringify({lng,lat}))
→ Python: on_click(MapClickEvent(lng, lat)) ← 上行回调
templates/map_template.html
→ <script type="module"> import bicMap from 'bic-map-plugin.mjs'
→ import { createBridge } from 'bridge-adapter.mjs'
→ await bridge.initMap({baseUrl, cssPath, jsPath, ...})
→ bicMap.init({cssUrl, jsUrl, assetsUrl, fontsUrl})
→ 动态加载 bicMap.min.js (MapLibre + Three.js + Turf.js)
→ 动态加载 bicMap.css
→ bicMap.createMap({container, center, zoom})
→ 基于 bicMap.maplibregl 创建地图实例
→ map.on('load') → pywebview.api.on_map_ready()
→ Python BicMapClient._ready.set() → 触发 on_ready 回调
bic-map-python/
├── bicmap_view.py # 视图容器:WebView + HTTP 服务器管理
├── bicmap_bridge/ # 桥接核心包
│ ├── __init__.py # 包导出
│ ├── client.py # BicMapClient:JS 通信调度 + 子命名空间 API
│ ├── types.py # 数据类型 (MapClickEvent, MapOptions, etc.)
│ └── errors.py # 异常定义
├── templates/
│ └── map_template.html # HTML 模板,ES Module 自动初始化 SDK
├── web/
│ └── bridge-adapter.mjs # JS 侧桥接:路由表 + dispatch + 回调
├── examples/
│ └── slam_drawing_demo.py # SLAM 地图绘制 Demo
├── requirements.txt
└── screenshot.png # 示例运行截图输出
# 1. 创建虚拟环境
python3 -m venv venv
# 2. 激活
source venv/bin/activate
# 3. 安装依赖
pip install -r requirements.txt
# 4. 启动 Demo
python examples/slam_drawing_demo.py
# 完成后退出虚拟环境
# deactivatefrom bicmap_view import BicMapView
# 定义事件回调
def on_ready():
print("地图就绪,开始操作")
view.bridge.marker.add(116.397428, 39.90923, "天安门")
view.bridge.invoke("", "polyline",
coordinates=[[116.38, 39.90], [116.42, 39.92]],
color="#FF0000", width=4)
def on_click(event):
print(f"点击: lng={event.lng}, lat={event.lat}")
# 创建并启动
view = BicMapView(title="My Map", width=1024, height=768)
view.bridge.on_ready = on_ready
view.bridge.on_click = on_click
view.start() # 阻塞,直到用户关闭窗口视图容器,管理 WebView 窗口 + HTTP 服务器,持有 bridge 客户端。
| 方法 | 参数 | 说明 |
|---|---|---|
__init__ |
title, width, height, web_dir, template_path |
创建地图视图 |
start() |
— | 启动 HTTP 服务器和 WebView 窗口(阻塞) |
screenshot(save_path) |
保存路径 | 截图保存为本地 PNG 文件 |
close() |
— | 关闭窗口和 HTTP 服务器 |
核心桥接客户端,所有地图操作都通过 view.bridge.xxx() 访问。
| 方法 | 参数 | 说明 |
|---|---|---|
fly_to(center, zoom) |
[lng, lat] + 缩放级别 |
平滑飞行动画 |
fit_bounds(bounds, padding) |
[[lng1,lat1],[lng2,lat2]] + padding |
自适应边界 |
get_state() |
— | 返回 {center, zoom, bearing, pitch} |
screenshot() |
— | 返回 canvas 截图的 base64 data URL |
clear_layers(prefixes) |
图层 ID 前缀列表 | 清除指定前缀的图层 |
add_zoom_control(position) |
位置如 "bottom-right" |
添加缩放控件 |
wait_ready(timeout) |
超时秒数 (默认 30) | 阻塞等待地图就绪 |
所有创建方法返回 controllerId,可用于后续的 update/remove/show/hide 操作。
| 方法 | 说明 |
|---|---|
create_point_cloud(points, options) |
创建 2D 点云 |
create_point_cloud_3d(points, options) |
创建 3D 点云 |
create_rectangles(rects, options) |
创建矩形 |
create_polygons(polygons, options) |
创建多边形 |
create_circles(circles, options) |
创建圆形 |
create_polylines(lines, options) |
创建折线 |
create_wide_lines(lines, options) |
创建宽线 |
update(controller_id, data) |
更新图形数据 |
remove(controller_id) |
移除图形 |
show(controller_id) |
显示图形 |
hide(controller_id) |
隐藏图形 |
| 方法 | 说明 |
|---|---|
add(lng, lat, label) |
添加简单标记点(原生 Popup) |
add_directional(lng_lat, options) |
添加可旋转方向标记 |
add_batch_poi(points, options) |
批量添加 POI 标记 |
add_robot(robots, options) |
批量添加机器人标记 |
| 方法 | 说明 |
|---|---|
enable_rectangle(options) |
启用矩形绘制 |
enable_circle(options) |
启用圆形绘制 |
enable_polygon(options) |
启用多边形绘制(完成后触发 on_draw_complete) |
enable_polyline(options) |
启用折线绘制(完成后触发 on_draw_complete) |
disable(controller_id) |
停止绘制工具 |
finish(controller_id) |
完成绘制 |
| 方法 | 说明 |
|---|---|
add_geojson_source(source_id, data) |
添加 GeoJSON 数据源 |
add_fill(layer_id, source_id, paint) |
添加填充图层 |
add_line(layer_id, source_id, paint) |
添加线图层 |
| 方法 | 说明 |
|---|---|
get_asset_path(relative_path) |
获取资源文件 URL |
get_font_path(font_file) |
获取字体文件 URL |
get_cursor_style(path, x, y, fallback) |
获取自定义光标样式 |
| 方法 | 说明 |
|---|---|
load(options) |
异步加载 SLAM 地图,返回结果 dict |
通过 view.bridge.on_xxx 设置:
| 属性 | 触发时机 | 参数类型 |
|---|---|---|
on_ready |
地图 load 事件完成 | 无参数 |
on_click |
用户点击地图 | MapClickEvent(lng, lat) |
on_draw_complete |
绘制工具完成绘制 | DrawCompleteEvent(type, geojson, ...) |
| 类型 | 说明 |
|---|---|
LngLat |
Tuple[float, float] |
MapClickEvent |
lng, lat |
DrawCompleteEvent |
type, geojson, info, data |
MapState |
center, zoom, bearing, pitch |
MapOptions |
container, center, zoom, pitch, bearing |
PolygonStyle |
fill_color, fill_opacity, outline_color, outline_width |
PolylineStyle |
color, width, opacity, show_arrow |
DirectionalMarkerOptions |
image_path, size, initial_rotation, draggable |
POIMarkerData |
lng_lat, rotation, name |
RobotMarkerData |
lng_lat, rotation, name, robot_id |
| 异常 | 说明 |
|---|---|
BridgeError |
桥接基础异常 |
BridgeNotReadyError |
桥接未就绪——地图尚未加载完成时调用 API |
BridgeEvalError |
JS 执行错误——dispatch 内部异常 |
BICMap-Python 基于以下项目构建:
| 资源 | 链接 |
|---|---|
| BICMap GL 主工程 | GitHub |
| 在线文档 & 示例门户 | bicmap.x-humanoid-cloud.com |
感谢以下开发者对 BicMap 的贡献:
lxycreate |
MIT © 2024-2026 北京人形机器人创新中心 (Beijing Innovation Center of Humanoid Robotics)
本项目打包并再分发了若干第三方开源库,其版权与许可证声明见 THIRD-PARTY-LICENSES.md。
- pywebview 版本: >= 5.0,macOS 使用 Cocoa/WKWebView 后端
- HTTP 服务器: Python 内置
http.server,动态端口,后台 daemon 线程运行 - CORS: 所有响应自动添加
Access-Control-Allow-Origin: * - JS Bridge:
bridge-adapter.mjs通过window.__bicMapBridge.dispatch()统一路由,按ns.action分发到对应处理函数 - Python 桥接:
BicMapClient通过evaluate_js注入 JS,invoke(ns, action, **params)序列化为 JSON 调用 dispatch - 异步操作:
slam.load()通过_invoke_async+on_result回调实现异步等待 - 回调流转: JS
pywebview.api.on_xxx()→BicMapView.on_xxx()→BicMapClient._on_xxx()→ 用户回调 webview.start()是阻塞调用,操作逻辑需放在on_ready回调中执行