地图上出现一个标签,背后同时有地理坐标、地图投影、瓦片请求、样式渲染与业务选择状态。把经纬度画成几个可点击方块,只能验证控件;一个可维护的地图应用,还要解释坐标来自哪里、底图谁提供、聚合在什么尺度发生,以及数据刷新后选中对象是否仍然有效。
本页采用 MapLibre GL JS,它是 WebGL 地图引擎,具有与 Mapbox GL JS 同类的 source / layer / camera 架构。默认加载真实 OpenStreetMap 街道瓦片;叠加的 P01—P30 是明确标注的合成设备点位,用于复现选择、过滤与快照更新。街道属于真实地理数据,设备数据属于实验输入,两者分别注明来源。
先决定地图引擎和地理数据各由谁负责
地图引擎负责投影、相机、瓦片调度、渲染和交互。底图服务提供瓦片、样式、字体或图标,业务接口则提供设备、区域或轨迹。换一个引擎,不会自动获得可用地图数据;购买底图服务,也不会替你处理选择状态、错误恢复与坐标转换。
MapLibre 让本例沿用现有开源引擎,无须引入个人 Mapbox Token。Mapbox GL JS 可对接 Mapbox 样式、地形、卫星影像等服务,接入时需要按项目实际使用的服务配置凭据与访问限制,并核对其计费和许可。二者不能靠改一个包名就保证所有扩展、样式和 API 都兼容。当前项目锁定 MapLibre 6.8.0,按安装版本的类型与 Worker 布局验证。
| 对象 | 本例实际输入 | 换成业务系统时要明确的契约 |
|---|---|---|
| 引擎 | MapLibre GL JS | 版本、浏览器/WebGL 支持、Worker 与 CSP |
| 地理底图 | OpenStreetMap 栅格街道瓦片 | 服务商、许可、缓存、限额、失败策略 |
| 点位 | 30 / 24 个合成点的快照 | 坐标参考系、稳定 ID、数据时刻、权限与覆盖范围 |
| 样式 | 本地 style v8 + 点位聚合 | 图层顺序、缩放范围、字体/图标与标签语言 |
| 相机 | 中心、zoom、pitch、bearing | 全部点位、选中定位、二维/三维和用户操作优先级 |
MapLibre 官方介绍说明其 WebGL 地图能力;Mapbox 官方指南可用于另一条引擎/服务接入路径。本站的选型依据是已有运行时与本案例的交互需求,不把某个引擎名字当作“专业”的证明。
经纬度还没有变成屏幕坐标
GeoJSON 使用经度在前、纬度在后,即 [longitude, latitude]。本例 P01 位于 [120.147, 30.264],在杭州附近。若写成 [30.264,120.147],纬度超出合法范围;即使两个值都合法,交换顺序也会把点送到另一个地区。经纬度单位是度,不是米。
RFC 7946定义 GeoJSON 坐标约定。通常输入是 WGS 84 经纬度,而常见街道瓦片按 Web Mercator 组织。地图引擎先把经纬度投影到平面,再用相机变换得到像素。Mercator 的东西向坐标与经度成正比,南北向坐标是纬度的非线性函数;越靠近极地变形越显著,常见瓦片范围截止到约 ±85.0511°。
x = (longitude + 180) / 360
φ = latitude × π / 180
y = (1 − ln(tan(φ) + sec(φ)) / π) / 2
瓦片索引 tx=floor(x × 2^z),ty=floor(y × 2^z)这里 x、y 是归一化世界平面坐标,z 是瓦片层级。缩放级别越高,视野越小;地图按视口请求对应瓦片,缓存已取得的数据。地图相机允许小数 zoom,实际瓦片选择还受数据源、像素比和过度缩放策略影响,不能把小数 zoom 原样填入 URL。
地图上的像素距离不等于地面距离。聚合半径是屏幕尺度,米制缓冲区、面积与线路长度则要用适合业务地域的投影或测地线方法。中国境内若业务点来自 GCJ-02/BD-09,需要先识别源坐标系与服务约定,使用明确、可验证的转换;给所有点硬加一个经纬度偏移会产生位置依赖误差。本页合成输入直接使用 WGS 84 约定。
样式把 source 和 layer 分开
source 声明数据从哪里来,layer 声明这些数据如何绘制。同一份点位 source 可以同时供聚合圆、数量标签和未聚合点层使用;多个图层引用同一 source,不需要复制业务数据。栅格 source 收到的是已着色的图片,矢量 source 收到的是可由样式继续表达的要素。
本例把 OSM 栅格放在业务点位下方。只切换该图层的 visibility,不调用 setStyle,因此点位 source、选择与相机都能保留。若换整张矢量样式,样式内的业务 source/layer 可能一并被替换,应在样式完成加载后按唯一 ID 重建,不能继续引用旧对象。
// map-scene.ts 的真实底图接入形状
map.addSource('osm', {
type: 'raster', tileSize: 256,
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
attribution: '© OpenStreetMap contributors',
});
map.addLayer({ id: 'osm', type: 'raster', source: 'osm' }, 'point-query');
// points: GeoJSON source,cluster=true,clusterRadius=48,clusterMaxZoom=16。
// 每条 Feature 用稳定的 point.id;地图显示与列表选择引用相同 ID。开启真实底图时,本地合成道路、河流、建筑脚印与挤出体全部隐藏,避免虚构街区覆盖真实道路。关闭底图或请求失败后,页面明确标注本地示意图。倾斜 55° 是相机动作:一张栅格图片不会因为相机倾斜就产生真实建筑高度。需要三维城市时,应接入有许可和高度字段的建筑矢量数据,使用 fill-extrusion 的米制 height/base;需要地形则还要 DEM source 与 terrain 设置。
跑一次选择、聚合和快照更新
先从列表选择 P01。选中 ID 写入 React 状态,地图移动到对应经纬度、标签高亮、弹窗读取当前数据。切换三维后再选 P02,pitch 保持,中心与 zoom 改变;回正北只复位 bearing。使用“全部点位”后点击一个聚合,观察展开前后的标签数。
随后选择 P30,切到快照 B:B 只有 P01—P24,旧选择被清除。选 P01 再切 A/B,因为 ID 仍存在,选择保留,但经纬度和数值按新快照刷新。搜索与分组影响的是同一个可见点位集合,列表数量、source 数据和聚合统计应一起改变。