第09章 - 坐标系统、投影与测量
GeoLibre 内部只有一套坐标约定:矢量数据永远是 WGS84 经纬度(EPSG:4326)、经度在前。所有与”投影”有关的事情都发生在这套约定的外围——导入时把别的 CRS 折进来,渲染时把经纬度画到某种平面或球面上,量算时按某个天体的半径把角度换算成米。理解了这三层,投影相关的绝大多数”数据跑到海里”“面积差十倍”“量出来的距离不对”都能在几分钟内定位。
本章按”概念 → 三层模型 → 各引擎差异 → 坐标读数 → 测量 → 高程 → 比例尺 → 排错 → 转换入口”的顺序展开。菜单项、参数名、偏好键名保留英文原文,叙述用中文。
说明:本章基于 GeoLibre 3.4(仓库 HEAD 354ee2fa)。事实来源为
docs/user-guide/map-controls.md、docs/user-guide/interface.md、docs/user-guide/settings.md、docs/maplibre-renderer.md、docs/arcgis-renderer.md与源码packages/core/src/{map-crs,ellipsoids,datum-shift,types}.ts、apps/geolibre-desktop/src/lib/{crs-catalog,crs-utils,coordinate-format,dms,geometry-measure}.ts、packages/core/src/map-crs.ts。
9.1 CRS 的三层含义与书写约定
9.1.1 地理坐标系、投影坐标系与工程坐标系
“坐标系”在 GIS 里其实混着三件事。区分它们,是读懂后面所有参数的前提。
| 名称 | 英文 | 坐标形式 | 单位 | 典型例子 |
|---|---|---|---|---|
| 地理坐标系 | Geographic CRS | 经度、纬度(可带高程) | 度 | EPSG:4326(WGS 84)、EPSG:4490(CGCS2000) |
| 投影坐标系 | Projected CRS | 东坐标 E、北坐标 N | 米或英尺 | EPSG:3857(Web Mercator)、EPSG:32650(UTM 50N)、EPSG:4547(CGCS2000 / 3-degree Gauss-Kruger CM 114E) |
| 工程坐标系 | Engineering CRS | 任意局部平面 | 任意 | 施工坐标、屏幕像素坐标 |
一个投影坐标系 = 地理坐标系(含基准面)+ 投影方法 + 参数(中央经线、原点纬度、假东假北、比例因子)+ 单位。所以 EPSG:32650 与 EPSG:32651 的区别只是中央经线差了 6°;EPSG:32650 与 EPSG:4547 的区别不只是分带宽度(6° vs 3°),还包括基准面(WGS 84 vs CGCS2000)——后者的坐标偏差在米级。
提示:GeoLibre 的界面里不会出现”工程坐标系”这个概念。所有 CRS 都通过 EPSG 代码或预设名表达,工具参数只接受 EPSG 数字代码(见 9.3)。
9.1.2 EPSG 代码、AUTHORITY:CODE 与 URN
同一个 CRS 有至少四种写法,GeoLibre 的不同代码路径分别容忍其中一部分:
| 写法 | 示例 | 出现位置 |
|---|---|---|
| 裸数字 | 4326 |
Processing 工具的 epsg / dst_epsg 参数、状态栏 EPSG 格式输入框 |
| 短前缀 | EPSG:4326 |
CRS 目录参数、GeoJSON 的 crs 成员、WFS 请求 |
| URN | urn:ogc:def:crs:EPSG::4326 |
老式 GeoJSON crs.properties.name、GML srsName |
| 带空格 | EPSG 4326、epsg4326 |
用户在检索框里手打的形式 |
两处解析函数的容错范围不同,值得记住:
parseEpsgCode(crs-catalog.ts):先去掉urn:ogc:def:crs:与EPSG前缀(冒号、空格都可选),剩下的部分必须只由 4–6 位数字构成。因此layer4326、EPSG:4326 EPSG:3857、EPSG:326都返回null,不会在半输入状态下显示一个自信的 CRS 名。parseEpsgCodeInput(coordinate-format.ts):只接受^(?:EPSG\s*:*\s*)?(\d{1,6})$,所以状态栏的 EPSG 输入框允许3857、EPSG:3857、epsg 32618,但不接受 URN。
9.1.3 轴序:EPSG 权威顺序 vs GeoJSON 经度优先
这是跨软件交换数据时最容易被静默搞错的一环。
- EPSG 数据集对地理坐标系登记的是纬度在前(
+axis=neu),这也是urn:ogc:def:crs:EPSG::4326在 WMS 1.3.0 里的含义。 - GeoJSON(RFC 7946)规定坐标是经度在前。
- GeoLibre 内部、图层属性、SQL 表、量算函数,全部按经度在前处理。
GeoLibre 在几个边界上做了归一:
- 读 EPSG 定义时,
epsg-proj4.ts会把 proj4 定义串里的+axis=...剥掉(raw.replace(/\+axis=\w+\s*/g, "")),并单独记录northFirst标志,让转换器始终按东-北顺序工作; - WMS 1.1.1 的
BBOX恒为东、北顺序,所以自定义 CRS 的 WMS 请求一律走 1.1.1 而不是 1.3.0(见 9.5.4); - 行星预设的 WMS 请求即使底图是”简单圆柱”,也用
EPSG:4326标签请求,避开服务器的 IAU 代码解析问题。
警告:从 QGIS、ArcGIS Pro 里复制出来的经纬度文本经常是”纬度, 经度”。粘贴进 View → Set View 的 Paste coordinates 时它按 GeoLibre 的约定解析(经度在前);如果你的数字明显越界(纬度 > 90),先怀疑轴序,而不是坐标本身。
9.1.4 单位、基准面与 towgs84
单位问题在投影坐标系里最直观:EPSG:27700 是米,EPSG:2227 是美制英尺。单位错了,缓冲区半径、栅格分辨率、里程统计会成比例错,而位置是对的——这种”位置对、量算错”的现象几乎总是单位问题。
基准面问题则更隐蔽:即使投影参数完全正确,两个基准面之间的偏移如果不补,数据会整体平移几十米到几百米。GeoLibre 用一张手工维护的表补这件事(packages/core/src/datum-shift.ts),因为它的离线 EPSG 表(geotiff-geokeys-to-proj4)对很多国家网格不带 +towgs84:
| 基准面 | 覆盖的 EPSG | +towgs84(米 / 角秒 / ppm) |
|---|---|---|
| OSGB 1936 | 4277、27700 | 446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 |
| DHDN | 4314、3068、31466–31469 | 598.1,73.7,418.2,0.202,0.045,-2.455,6.7 |
| Amersfoort | 4289、28992 | 565.417,50.3319,465.552,-0.398957,0.343988,-1.8774,4.0725 |
| CH1903 / CH1903+ | 4149、21781 / 4150、2056 | 674.4,15.1,405.3,0,0,0,0 / 674.374,15.056,405.346,0,0,0,0 |
| Monte Mario | 4265、3003、3004 | -104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 |
| GGRS87 | 4121、2100 | -199.87,74.79,246.62,0,0,0,0 |
| Hong Kong 1980 | 4611、2326 | -162.619,-276.959,-161.764,0.067753,-2.24365,-1.15883,-1.09425 |
| NAD27(美国本土均值) | 26710–26719 | -8,160,176,0,0,0,0 |
补丁的注入点有三个,全部经由同一个函数:
// packages/core/src/datum-shift.ts(节选)
export function withDatumShift(definition, crs, datum) {
// 已经写了基准面变换的定义不动,避免覆盖用户/上游的选择
if (/\+(towgs84|datum|nadgrids)=/.test(definition)) return definition;
const shift = datumShiftFor(crs, datum);
return shift ? `${definition.trim()} +towgs84=${shift}` : definition;
}
resolveEpsgProjection(packages/map/src/epsg-proj4.ts)——把 EPSG 代码解析成 proj4 定义时;withGeoKeysDatumShift——从 GeoTIFF 的 geokeys 建立 COG 源 CRS 时(installCogTilerDatumShift把它挂到cog-tiler-wasm的setSourceCrsResolver上);- WMS 与 GML 重投影路径。
注意:这些是 EPSG 公布的 Helmert 参数,与 PROJ 的最优变换差几米(NAD27 用美国本土均值,最差约 15 m)。如果需要厘米级,必须用 OSTN15 / NTv2 / NADCON 这类格网变换,而格网文件不在 proj4 的交付范围内——GeoLibre 不提供,也不声称提供。
9.2 GeoLibre 的 CRS 分层
9.2.1 数据层:矢量图层恒为 EPSG:4326
docs/user-guide/adding-data.md 把这条写得非常直接:
Vector files are reprojected to EPSG:4326 on load.
这条约定的后果是一连串”顺手”:
- 图层的
geojson字段是标准FeatureCollection,坐标是度; - 属性表里的
$length/$area只需要一个半径就能出米(9.7.8); - SQL 工作区里注册的表,几何列是
geom,坐标是度; - 导出为 GeoJSON / CSV / 属性表时不需要再问”要哪个 CRS”;
- Processing 的客户端工具(Turf.js)能直接算,因为它只认经纬度。
源 CRS 的探测渠道按格式不同:
| 格式 | 源 CRS 来自哪里 | 探测失败时的行为 |
|---|---|---|
| GeoParquet | geo 元数据块的 crs 字段 |
声明为 "crs": null 表示”无已知参考系”,按原样画、不转换 |
| Shapefile | .prj 侧车文件 |
无 .prj 时按 WGS84 处理 |
| GeoPackage / GML / FlatGeobuf / DXF / TAB | GDAL 图层元数据(ST_Read_Meta 的 authority code) |
无元数据则不变换 |
| CSV / 分隔文本 | 用户手选的 源 CRS | 默认按 WGS84 |
| CAD(DXF/DWG) | 无(CAD 不带坐标系) | 必须手选 EPSG |
对最后一种”老式 GeoJSON”,crs-utils.ts 提供一个专门的判定:
// apps/geolibre-desktop/src/lib/crs-utils.ts
export function isGeographicCrs(crs) {
const value = (crs ?? "").replace(/\s+/g, "").toUpperCase();
if (!value) return true;
return value.includes("CRS84") || /EPSG:+4326\b/.test(value);
}
export function projectedGeoJsonCrs(value) {
const name = value?.crs?.properties?.name;
if (typeof name !== "string" || isGeographicCrs(name)) return null;
const match = name.toUpperCase().match(/EPSG:+(\d+)/);
return match ? `EPSG:${match[1]}` : null;
}
也就是说:
- 缺省、
CRS84、EPSG:4326(含EPSG: 4326这种带空格写法)→ 走”便宜路径”,不做任何重投影; - 任何其它 EPSG 代码 → 返回
EPSG:<code>,由reprojectFeatureCollectionToWgs84交给 DuckDB 的ST_Transform; crs成员缺失或畸形 → 返回null,按 WGS84 画。
警告:最后一条是”善意默认”,不是修复。一份坐标是 EPSG:26911 米制、却漏了
crs成员的 GeoJSON,会被当成度数画到几内亚湾附近(x值被当成经度、y值被当成纬度并被夹紧)。判断方法:图层位置整体落在 (0,0) 附近或贴着 ±180/±90 的边缘。
9.2.2 项目层:preferences.map 里的 CRS 键
“项目 CRS”在 GeoLibre 里不是一个值,而是一组键,全部存在 .geolibre.json 的 preferences.map 下:
| 偏好键 | 类型 | 默认 | 作用 |
|---|---|---|---|
mapCrs |
string |
缺省 | MapLibre 平面地图的 CRS:预设 id 或 "EPSG:<code>";缺省 = Web Mercator / 地球 |
arcgisWkid |
number |
缺省 | ArcGIS 平面 MapView 的投影,WKID(54099 Spilhaus、54030 Robinson 等);缺省 = Web Mercator |
projection |
"globe" \| "mercator" |
"globe" |
地球投影形态,对应地图上的 Globe 按钮 |
ellipsoidId |
string |
"earth" |
量算使用的天体(12 个之一,见 9.8) |
scaleUnit |
"metric" \| "imperial" \| "nautical" |
"metric" |
比例尺、Eye alt、Elev、快速分析缓冲预设的单位制 |
coordinateFormat |
string |
"dd" |
状态栏坐标标注法(7 种,见 9.6) |
coordinateEpsgCode |
number \| undefined |
缺省(=3857) | "epsg" 格式使用的 EPSG 代码 |
showPointerElevation |
boolean |
false |
状态栏 Elev 读数开关 |
terrainEnabled |
boolean |
false |
3D 地形开关 |
restrictBounds / bounds |
boolean / [w,s,e,n] |
false / [-180,-85,180,85] |
平移限制 |
这组键的设计取舍值得点出来:它们是”项目”的一部分,不是”用户”的一部分。同一台机器开两个工程,一个用北极极射投影、一个用 Web Mercator,互不影响;把工程文件发给同事,对方的打开效果和你一致。代价是首选项(比如界面语言)不能靠这套机制承载,那是设备级的(见第04章)。
9.2.3 渲染层:Web Mercator 是默认,但它是”缺省”而不是”硬编码”
MapLibre 引擎的默认投影是 Web Mercator 平面或 globe(由 projection 决定)。判断”当前是不是默认地图”的规则在 map-crs.ts:
const WEB_MERCATOR_CODES = new Set([3857, 900913, 102100, 102113]);
export function normalizeMapCrs(value) {
if (typeof value !== "string") return undefined;
if (getMapCrsPreset(value)) return value;
const code = parseMapCrsEpsgCode(value);
if (code === null || WEB_MERCATOR_CODES.has(code)) return undefined;
const epsg = `EPSG:${code}`;
// 预设本身占用的 EPSG 代码(3413、3031、4326)直接归一到该预设:
// 它会带上配套底图,而且 MapLibre 每个代码只能注册一套网格
return MAP_CRS_PRESETS.find((preset) => preset.code === epsg)?.id ?? epsg;
}
四条推论:
900913、102100、102113是 3857 的历史别名(Google、Esri 各一套),写进项目文件也不会让地图变成”自定义 CRS”;EPSG:3413、3031、4326三个代码会被归一到对应预设 id,因为它们各自带底图;- 其它任何代码 →
"EPSG:<code>",此时地图进入”无底图的自定义平面 CRS”模式(9.5.2); - 值畸形(非字符串、
EPSG:abc)→ 当作缺省,不做任何事,也不会报错。
9.2.4 导入 → 渲染的完整链路
把前面几节串起来,一份”坐标是 EPSG:27700 的 Shapefile”进到地图上,实际发生了五步:
- 读取器拿到文件,从
.prj读出EPSG:27700;若声明是 4326(或 3857 带 z),走”便宜路径”,否则走重投影; reprojectFeatureCollectionToWgs84用ST_Transform(spatial 扩展,proj4/PROJ 提供数学)把每个坐标转成经纬度;epsg-proj4.ts在解析 27700 时给定义补上 OSGB 1936 的+towgs84(9.1.4),否则整体偏约 100 m;- 结果作为
FeatureCollection交给addGeoJsonLayer,图层里存的是经纬度; MapController.syncLayers把图层交给 MapLibre;渲染时按当前mapCrs决定是画在 Web Mercator、球面,还是某个平面网格上。
提示:如果第 1 步判定错误(例如 GeoParquet 的
crs写的是OGC:CRS84而实际是投影坐标),链路后面每一步都会”正确地”画错位置。排查时先看 Metadata 面板里的图层信息,再怀疑渲染。
9.3 crs-catalog.ts:工具箱里的 EPSG 目录
9.3.1 为什么要自建目录
reproject_vector(Whitebox 的 Reproject Vector)与 assign_projection_vector(Assign Projection Vector)这类工具,参数就是一个裸 EPSG 数字。在没有目录之前,对话框把它渲染成一个数字输入框——用户必须凭记忆背出 32650 是”WGS 84 / UTM zone 50N”,4547 是”CGCS2000 / 3-degree Gauss-Kruger CM 114E”。仓库把这件事记在 issue #1538 下,并在 crs-catalog.ts 的文件头写明了取舍:
The catalog is curated rather than exhaustive (the EPSG dataset holds ~6000 geographic and projected systems, too much to bundle)
所以它选择了”精选 + 保留自由输入“:目录里的条目给检索和名称提示,自由文本仍然是参数的真实来源——目录里没有的代码照样能输入、照样会被工具接受。
9.3.2 目录结构
export type CrsKind = "geographic" | "projected";
export interface CrsEntry {
code: number; // 工具期望的数字 EPSG
name: string; // EPSG 官方名称,例如 "WGS 84 / UTM zone 17N"
kind: CrsKind; // 供选择器分组
}
名称用的是 EPSG 官方数据集名称,不是本地化译名。这一点很实用:从 QGIS 或 PROJ 里抄来的标签能和这里逐字对上,formatCrsLabel(entry) 输出的 WGS 84 / UTM zone 17N (EPSG:32617) 也是同一套字符串。
9.3.3 301 条的构成
CRS_CATALOG 由三段拼成,顺序即默认列表顺序(按”多常被用到”排):
| 段 | 条数 | 内容 |
|---|---|---|
GEOGRAPHIC_CRS |
37 | 地理坐标系,WGS 84 起头,接着 NAD83、NAD83(2011)、NAD27、ETRS89、GDA94/GDA2020、OSGB36、ED50、CH1903/CH1903+、CGCS2000、New Beijing、JGD2000/JGD2011、Tokyo、Hong Kong 1963(67)、SIRGAS 2000、PZ-90 等 |
NAMED_PROJECTED_CRS |
56 | 命名投影坐标系:世界与极地投影(3857、3395、8857、3413、3031、3995、3976)、北美(5070、6350、3338、9311、3347、3978)、欧洲各国网格(3035、3034、27700、2154、28992、2056、3067、3006、5514、23700、3765、3844、2100、2180…)、亚太(3414、2326、3826、5179、5186、2193、3577、9473、7845、3112…)、南美与非洲(5880、31983、3116、2048、2055) |
| 分区族 | 208 | 6 个 UTM/MGA 族 + 日本 19 个平面直角 + 中国 11 个高斯-克吕格带 |
分区族的具体跨度按 EPSG 实际登记的范围生成(这是目录里最容易写错、也最容易验证的部分):
| 族 | EPSG 范围 | 条数 | 命名模式 |
|---|---|---|---|
| WGS 84 / UTM 北半球 | 32601–32660 | 60 | WGS 84 / UTM zone <n>N |
| WGS 84 / UTM 南半球 | 32701–32760 | 60 | WGS 84 / UTM zone <n>S |
| NAD83 / UTM | 26901–26923 | 23 | NAD83 / UTM zone <n>N |
| ETRS89 / UTM | 25828–25837 | 10 | ETRS89 / UTM zone <n>N(38N 已弃用,故止于 37) |
| GDA94 / MGA | 28348–28358 | 11 | GDA94 / MGA zone <n> |
| GDA2020 / MGA | 7846–7859 | 14 | GDA2020 / MGA zone <n>(2020 实现新增了 46/47/59 等外圈带) |
| JGD2011 平面直角 | 6669–6687 | 19 | JGD2011 / Japan Plane Rectangular CS I…XIX |
| CGCS2000 高斯-克吕格 | 4491–4501 | 11 | CGCS2000 / Gauss-Kruger zone 13…23 |
tests/crs-catalog.test.ts 里专门有三条测试守这些边界:族名与 EPSG 一致、族跨度不多不少(25838、28347、28359、7860、26924、4502 都必须是”未知代码”)、以及全表无重复代码。
9.3.4 检索规则
searchCrsCatalog(query, limit) 的行为可以归纳成五条:
- 空白查询返回整张表,保持精选顺序(
limit默认等于表长,所以默认列表不会被”截断在族中间”); - 忽略
EPSG前缀(冒号、空格可选),因此epsg:32617、EPSG 32617、EPSG32617、32617等价; - 按空白分词,每个词都要命中(名字或代码),所以
utm 17n收窄到 zone 17 的北半球系; - 排序分四档:精确代码 > 代码前缀 > 名称前缀 > 名称包含;同档内保持目录顺序;
- 不认识的词就让结果为空,不做模糊纠错。
几个可以直接对照的结果:
| 查询 | 结果特征 |
|---|---|
4326 |
WGS 84 (EPSG:4326) 第一 |
utm 17n |
WGS 84 / UTM zone 17N、NAD83 / UTM zone 17N 都出现 |
cgcs2000 |
CGCS2000 地理系 + 11 个高斯-克吕格带 + China Geodetic Coordinate System 2000 |
lambert |
名称含 Lambert 的一批(3347、3978、2154、31370、3812、3112、7845…) |
326 |
32601–32660 一整段(代码前缀档) |
9.3.5 在对话框里怎么用
参数名以 epsg 结尾、类型是整数或浮点的参数,会被识别为 CRS 参数(isCrsParameter 的规则是 (^|_)epsg(_code)?$ 且 kind 为 int/double)。命中的字段右侧出现 Browse 按钮,点开是带分组的检索面板:
面板自上而下是:检索框 → Geographic 分组 → Projected 分组(如 WGS 84 / UTM zone 17N (EPSG:32617))→ 无结果时的提示 No matching coordinate system. Any EPSG code can still be typed.
提示:因为”自由文本是真实来源”,你完全可以在参数框里直接粘一个目录外的代码(例如
EPSG:3414之外的EPSG:24378)。目录只负责”帮你查到”,不负责”限制你只能选这些”。反过来说,如果工具报”no such projection”,先确认代码本身是否存在,再怀疑工具。
9.4 四种渲染引擎的投影能力
GeoLibre 的四个引擎对”投影”的支持是四套不同的机制,不是同一个功能的四种实现。把它们混为一谈,是”我设了 CRS 但地图没变”这类问题的根源。
| 引擎 | 平面投影能力 | 球面/三维 | 设置项 | 关键限制 |
|---|---|---|---|---|
| MapLibre | Web Mercator,外加任意已注册的平面 CRS(addProjection) |
globe(projection: "globe") |
preferences.map.mapCrs、projection |
瓦片不重投影,所以只有 GeoJSON、WMS/export 瓦片、图像/视频叠加能画;XYZ、矢量瓦片、MBTiles、Wayback 一律不画 |
| Mapbox | 仅 Web Mercator | globe + terrain | projection |
不支持自定义平面 CRS |
| Cesium | 3D 球体;SDK 自带 2D、Columbus 场景 | 原生 3D 地球 | — | 不吃 mapCrs/arcgisWkid;与其它 pane 同步时按地面分辨率换算,不是直接对 zoom |
| ArcGIS | 平面 MapView 可画任意 WKID 的投影 |
SceneView(global 或 local) |
preferences.map.arcgisWkid |
另一投影下不画底图与瓦片图层;COG 会被 warp;相机、指针坐标、选择都在投影与经纬度之间换算 |
9.4.1 MapLibre:唯一一个”两套投影”的引擎
MapLibre GL JS 6.15 起支持任意平面 CRS(maplibregl.addProjection)。仓库在 packages/core/src/map-crs.ts 的文件头把机制讲清楚了:
tiles are placed on a square quad grid laid over the CRS plane and are not reprojected, so every tile has to be made in that CRS already. A WMS does that on request (the
{bbox}tile token asks for each tile’s box in CRS units)
这句话是理解 9.5 全部限制的钥匙:MapLibre 只是把瓦片摆到 CRS 平面的方格上,它不会把 Web Mercator 瓦片”投影”成别的样子。所以要么服务器按该 CRS 出瓦片(WMS 可以),要么数据本身是经纬度由 proj4 在 worker 里现算(GeoJSON 可以),要么就没法画。
9.4.2 Mapbox:与 MapLibre 共享 Style Spec,但不共享投影
Mapbox 引擎能画的投影只有 Web Mercator 与 globe(mapbox-renderer.md 明确写着 “MapLibre (globe and Mercator)” 与 “the Mercator/globe projection”)。同时它有一批图层强制要求 Mercator:
- Zarr 层的 3D 视图;
- 本地 NetCDF/HDF 立方与 Kerchunk 引用(未分块,直接画在 Web Mercator);
- 部分 deck.gl 与 3D 内容。
在 globe 视图下添加这类图层,地图会自动切回 Mercator,而不是把图层藏起来。这是有意的:宁可切投影,也不让用户看到”加载成功但什么也没有”。
9.4.3 Cesium:不是”投影”,是”场景”
Cesium 的 pane 根本不参与 mapCrs / arcgisWkid 这套设置。它提供 3D、2D、Columbus 三种场景模式,相机与 GeoLibre 共享的 longitude/latitude/zoom/bearing/pitch 之间做双向翻译。多 pane 同步时,按地面分辨率对齐而不是按 zoom 对齐:
Mixed panes synchronize by ground resolution rather than raw zoom, keeping their apparent scales aligned even when panes have different sizes.
这就是为什么把一个 800×600 的 MapLibre pane 和一个 1600×900 的 Cesium pane 并排,两边的”比例尺感觉”是一致的,而状态栏显示的数字 zoom 可能不同。
9.4.4 ArcGIS:可换投影的平面视图
ArcGIS 是四个引擎里唯一能在平面上换投影、并且自带一整套重投影逻辑的:
| 设置 | 值 | 效果 |
|---|---|---|
| Settings → Map → ArcGIS map projection | Spilhaus (54099)、Equal Earth (8857)、Robinson (54030)、Winkel Tripel (54042)、Mollweide (54009)、Plate Carrée (4326)、Arctic (3995)、Antarctic (3031) | 平面 MapView 换成该投影 |
| 同上 | Custom WKID… | 任意 Esri 或 EPSG 代码 |
| 存储 | preferences.map.arcgisWkid |
缺省 = Web Mercator |
在这个模式下(docs/arcgis-renderer.md 的 “Map projections” 小节):
- 矢量图层(GeoJSON、要素服务、符号化包生成的伴生图层)由 SDK 的
projectOperator在客户端逐要素重投影; - COG 会被 warp 到该投影(
arcgis-reprojected-tiles.ts):视图投影分块,每块用 33×33 的采样格点把 Web Mercator 瓦片的内容插值填进来;跨投影接缝(Spilhaus 会把陆地切开)的格子逐像素投影;Web Mercator 止于 ±85.05°,所以极冠是空的; - 底图不画:Esri 底图样式与 XYZ 瓦片不会被 warp,只有 Blank 背景色;
- 相机、指针坐标、识别、选择、范围绘制都在投影与经纬度之间换算,所以共享相机、其它 pane、保存的视图仍然是度数;
- 即使开了地形,平面图仍然是
MapView(local scene 不能用该投影); - 未知 WKID → SDK 在地图上盖一层错误提示。
警告:
arcgisWkid只对 ArcGIS pane 生效。MapLibre、Mapbox、Cesium 的 pane 会忽略它——包括同一个工程的其它分屏。想让”全球视图用 Equal Earth、局部的用 Web Mercator”,正确做法是两个 pane 用不同引擎,而不是切换主图。
9.4.5 一张决策表
日常浏览与样式优先选 MapLibre + Web Mercator/globe;极地与行星选 MapLibre 的对应预设;需要”任意投影 + 底图”只能选 ArcGIS(有 key 时);三维地形、3D Tiles、CZML 选 Cesium。
9.5 MapLibre 的平面 CRS:预设、自定义与降级
9.5.1 预设清单
MAP_CRS_PRESETS 共 18 条:地球 3 条(底图走 NASA GIBS)、月球与火星 6 条(USGS Astrogeology WMS)、其余 9 个天体全为简单圆柱。
| 预设 id | 名称 | 代码 | 初始范围 | 底图 SRS | 底图 maxZoom |
|---|---|---|---|---|---|
earth-arctic |
Arctic Polar Stereographic | EPSG:3413 |
[-180, 60, 180, 90] |
EPSG:3413 | 7 |
earth-antarctic |
Antarctic Polar Stereographic | EPSG:3031 |
[-180, -90, 180, -60] |
EPSG:3031 | 7 |
earth-cylindrical |
Plate Carrée (WGS 84) | EPSG:4326 |
[-180, -90, 180, 90] |
EPSG:4326 | 7 |
moon-cylindrical |
Moon Simple Cylindrical | IAU_2015:30100 |
全球 | EPSG:4326 | 7 |
moon-north-polar |
Moon North Polar Stereographic | IAU_2015:30118 |
[-180, 70, 180, 90] |
EPSG:32661 | 8 |
moon-south-polar |
Moon South Polar Stereographic | IAU_2015:30120 |
[-180, -90, 180, -70] |
EPSG:32761 | 8 |
mars-cylindrical |
Mars Simple Cylindrical | IAU_2015:49900 |
全球 | EPSG:4326 | 7 |
mars-north-polar |
Mars North Polar Stereographic | IAU_2015:49918 |
[-180, 65, 180, 90] |
EPSG:32661 | 7 |
mars-south-polar |
Mars South Polar Stereographic | IAU_2015:49920 |
[-180, -90, 180, -65] |
EPSG:32761 | 7 |
mercury-cylindrical |
Mercury Simple Cylindrical | IAU_2015:19900 |
全球 | EPSG:4326 | 7 |
venus-cylindrical |
Venus Simple Cylindrical | IAU_2015:29900 |
全球 | EPSG:4326 | 7 |
io-cylindrical |
Io Simple Cylindrical | IAU_2015:50100 |
全球 | EPSG:4326 | 7 |
europa-cylindrical |
Europa Simple Cylindrical | IAU_2015:50200 |
全球 | EPSG:4326 | 7 |
ganymede-cylindrical |
Ganymede Simple Cylindrical | IAU_2015:50300 |
全球 | EPSG:4326 | 7 |
callisto-cylindrical |
Callisto Simple Cylindrical | IAU_2015:50400 |
全球 | EPSG:4326 | 7 |
titan-cylindrical |
Titan Simple Cylindrical | IAU_2015:60600 |
全球 | EPSG:4326 | 7 |
pluto-cylindrical |
Pluto Simple Cylindrical | IAU_2015:99900 |
全球 | EPSG:4326 | 7 |
charon-cylindrical |
Charon Simple Cylindrical | IAU_2015:90100 |
全球 | EPSG:4326 | 7 |
提示:IAU 代码的构成是”NAIF id × 100”。火星的 NAIF id 是 4,代码就是
49900(4+99+00);月球是 301 →30100。极射投影的 118/120 结尾分别表示北极/南极。
两组”服务器读不懂”的细节已由预设处理掉:简单圆柱网格一律用 EPSG:4326 标签请求(MapServer 的 PROJ 解析不了 IAU 代码),极射网格用 EPSG:32661/32761(USGS 就用这两个标签发布它们自己的极地图)。
9.5.2 自定义 EPSG
选 Custom EPSG code… 后输入任意编码,落到 mapCrs = "EPSG:<code>"。此时:
- 没有底图(没有为它准备好的服务器瓦片);
- 瓦片网格是拟合出来的,不是官方网格:
deriveTileMatrixSet以 2° 间隔采样经纬度格点,只保留”正反变换能回到原点、且局部尺度不超过最小尺度 2.5 倍”的点,把这些点的外包框加 4% 余量后取正方形,作为 zoom 0 的范围。
// packages/core/src/map-crs.ts(语义摘要)
const MAX_RELATIVE_SCALE = 2.5; // 边缘最多比最不畸变处拉伸 2.5 倍
// 采样 → 正反变换校验 → 计算每点局部尺度 → 丢弃超过 2.5 倍最小尺度的点
// → 用保留点的外包框构造居中正方形网格(side = max(w,h) * 1.04)
这套拟合解决了一个具体问题:极射投影能”回环”到接近另一极,但那里 1 km 会展开成几千个 CRS 单位,若按全定义域建网格,真正有用的区域在每个可用 zoom 上都是一个点。2.5 倍的阈值把南极极射投影的可用范围定在约 11°N,把横轴墨卡托定在中央经线两侧约 65°。
注意:自定义 EPSG 的 CRS 必须能被随包携带的 EPSG 表解析(
geotiff-geokeys-to-proj4+proj4)。表里没有的代码,或者 proj4 没实现的投影(例如 EPSG:6244 的+proj=col_urban),解析直接返回null,地图退回默认。
9.5.3 哪些图层能画
| 图层类型 | 在自定义平面 CRS 下 | 原因 |
|---|---|---|
| GeoJSON(含处理结果) | ✅ | proj4 在 MapLibre 的 worker 里逐要素投影 |
| WMS 瓦片 | ✅(服务器须支持该 CRS) | 请求每个瓦片的 {bbox}(CRS 单位) |
ArcGIS export / exportImage 瓦片 |
✅ | bboxSR/imageSR 设为该 EPSG |
| 图像 / 视频叠加 | ✅ | 按四角坐标摆放 |
| XYZ 瓦片、矢量瓦片、MBTiles、Wayback | ❌ 不画 | 它们是 Web Mercator 瓦片,MapLibre 不重投影 |
| 共享底图 | ❌ 不画 | 同上 |
同时有三条”顺带关闭”:
- Globe 按钮隐藏——平面 CRS 下没有球面;
- 地形关闭——地形是 Web Mercator 的高程网格;
- Restrict map bounds 失效——MapLibre 自己会把视图限制在 CRS 定义域内。
提示:切换 CRS 会重建整个 style并按新地图的范围取景;打开一个已保存的工程则保留它保存的视图。所以”切完 CRS 地图飞到别处”通常不是 bug,而是”新投影下的默认取景”。
9.5.4 WMS/ArcGIS 模板的改写规则
adaptBboxTemplateToCrs(template, code) 是这条链路上最关键的一段字符串处理,它有三条分支:
| 输入模板 | 返回值 | 处理 |
|---|---|---|
已含 {bbox} |
原样返回 | 已经是 CRS 无关的模板 |
含 {bbox-epsg-3857} |
改写后的 URL | 换成 {bbox},设置 SRS=<code>、VERSION=1.1.1、删除 CRS=;若是 ArcGIS export(有 bboxSR=),改为 bboxSR=<epsg>、imageSR=<epsg> |
| 两者都没有(XYZ/TMS) | null |
无法在其它 CRS 中绘制 |
两个细节值得抄进自己的实现:
- 参数替换是手写的正则,不是
URLSearchParams,因为后者会把 MapLibre 令牌{bbox}的花括号百分号编码; - WMS 版本被强制降到 1.1.1:1.3.0 会为地理坐标系交换
BBOX的轴序,而 1.1.1 的SRS恒为东、北顺序,与 MapLibre 的{bbox}语义一致。
9.6 坐标格式与状态栏
9.6.1 七种标注法
状态栏最左边的 Coords 支持七种格式,coordinate-format.ts 里定义为 COORDINATE_FORMATS:
| 格式 | 代码 | 示例 | 说明 |
|---|---|---|---|
| Decimal degrees | dd |
-83.92074, 35.96064 |
默认;经度在前;5 位小数(约 1 m) |
| Degrees, minutes, seconds | dms |
35°57'38.3"N 83°55'14.66"W |
纬度在前;秒保留 2 位 |
| Degrees, decimal minutes | ddm |
35°57.6384'N 83°55.2444'W |
纬度在前;分保留 4 位 |
| UTM | utm |
17S 236594mE 3983527mN |
带号 + 纬度带字母 + 东/北 |
| MGRS | mgrs |
17SKV3659483527 |
1 m 精度(5 位 easting/northing) |
| USNG | usng |
17S KV 36594 83527 |
与 MGRS 同一个格网,加空格 |
| Projected (EPSG:code) | epsg |
-9342014.04, 4295206.85 (EPSG:3857) |
x/y,任意 EPSG |
三条”排序约定”值得记住,因为它们决定了你复制出来的字符串能否被别的软件直接吃:
dd保持经度在前,与 GeoJSON 和整个应用一致;dms/ddm按书写习惯纬度在前;utm/mgrs/usng的带号跟着格网规则,与 MGRS 同一套(含挪威、斯瓦尔巴的例外)。
9.6.2 切换与保存
三条等价路径:
- 点击状态栏的坐标文本——循环到下一个格式(
nextCoordinateFormat,顺序就是上表的顺序,epsg之后回到dd); - Settings → Map Preferences → Coordinate format 直接选;
- 选
epsg时出现 EPSG 输入框,可手打3857也可以用 Browse 从 9.3 的目录里选。
选择随工程保存(preferences.map.coordinateFormat 与 coordinateEpsgCode)。coordinateEpsgCode 单独存放的好处是:在几种格式之间来回切,投影格式的代码不会被清掉。
9.6.3 边界与回退
这些回退都是有意的,而且回退到十进制度而不是显示一个错误的数字:
| 情况 | 行为 | 原因 |
|---|---|---|
纬度 < −80° 或 > 84° 用 utm |
回退 dd |
极区没有 UTM 坐标 |
极区用 mgrs/usng |
回退 dd |
那里是 UPS 格网(A/B/Y/Z),mgrs 包不实现 |
epsg 的代码未知 / 解析中 |
回退 dd |
createProjectedReadout 返回 null |
epsg 投影返回非有限值 |
回退 dd |
proj4 在定义域外可能给 Infinity |
| 经度超出 ±180(平移过日界线后) | 先归一化到 [−180, 180) 再格式化 | 否则会出现 190°0'0"E |
epsg 格式的小数位是按 CRS 类型分的:地理坐标系 6 位(约 0.1 m),投影坐标系 2 位(比指针能分辨的更细)。这个细节有实际意义——投影坐标动辄七位数,2 位小数已经过剩;而经纬度 4 位只有 11 m,不够用。
9.6.4 反向输入:哪些地方能吃格网坐标
与”显示”对称的”输入”有四处:
| 入口 | 接受的写法 |
|---|---|
| Layers 面板底部的地点搜索 + Go to | 地名;MGRS/USNG(18SUJ2337106519、18S UJ 23371 06519、粗精度 18SUJ23370651);UTM(18N 323394 4307395、18S 323394mE 4307395mN) |
| View → Set View → Paste coordinates | 同上;另有 DD/DMS/DDM 三态切换 |
| View → Set View 的分轴输入 | 经度、纬度、zoom、bearing、pitch 各自独立 |
| Controls → View State | 读/写精确的 center、zoom、bearing、pitch |
UTM 输入里的字母按纬度带解释:S 表示 32°N–40°N 带,除非北坐标与该带不符(那就按南半球解释)。这条规则让 18S 323394 4307395 这类”看起来像半球”的输入仍然落在正确位置。
9.6.5 状态栏的其它读数
| 读数 | 含义 | 与投影/CRS 的关系 |
|---|---|---|
| Elev | 指针处地面高程 | 见 9.9;随 scaleUnit 换单位 |
| GPS | GPS 追踪时的当前定位 | 设备经纬度,不受投影影响 |
| Zoom | 缩放级别,两位小数 | 引擎各自换算;Cesium pane 不按 zoom 对齐 |
| Eye alt | 相机海拔 | 按天体半径换算,且跟随 scaleUnit |
| Bearing / Pitch | 相机旋转与俯仰(度) | 与 maxPitch 相关 |
提示:Eye alt 是”相机海拔”,与”地面高程”(Elev)不是一回事。在倾斜视角下两者差得很多;在正上方时二者相差一个相机高度。做三维判读时不要混用。
9.7 测量:距离、面积与方位角
9.7.1 打开与三种模式
Controls → Measure 打开测量面板。面板有三个模式与一个单位选择器:
| 模式 | 语义 | 几何 |
|---|---|---|
| Distance | 折线长度 | 多段线,可任意加点 |
| Area | 面积 | 闭合多边形 |
| Circle | 半径圆 | 圆心 + 半径 |
9.7.2 操作流程
- Controls → Measure;
- 选 Distance / Area / Circle;
- 在 Unit 里选单位(见 9.7.3);
- 点 Start;
- 在地图上逐点点击加点;
- 双击或右键结束当前测量;
- 继续下一条,或点 Clear All 清空全部。
面板可以被拖动右下角缩放到 min(75vh, 900px),超过内容高度后会出滚动条;这是 makeMeasurePanelResizable 做的(宽 min(90vw, 560px)、最小 220×160)。
9.7.3 单位
测量面板的单位集合与属性表字段计算器的完全一致,这是刻意的设计——”算一列面积(公顷)”和”量一块面积”必须能对得上。
| 距离单位 | 符号 | 换算 |
|---|---|---|
meters |
m | 1 |
kilometers |
km | 1/1000 |
feet |
ft | 1/0.3048 |
yards |
yd | 1/0.9144 |
miles |
mi | 1/1609.344 |
nautical-miles |
nmi | 1/1852 |
| 面积单位 | 符号 | 换算(自 m²) |
|---|---|---|
square-meters |
m² | 1 |
square-kilometers |
km² | 1/1 000 000 |
hectares |
ha | 1/10 000 |
square-feet |
ft² | 1/0.09290304 |
acres |
ac | 1/4046.8564224 |
square-miles |
mi² | 1/2 589 988.110336 |
注意:面板的 Unit 与 Settings → Map Preferences → Scale bar units 是两个独立设置。前者只影响测量读数,后者影响比例尺、Eye alt、Elev 和右键快速分析的缓冲预设。以为”改了比例尺单位测量就会跟着变”是常见误解。
9.7.4 方位角:大圆初始方位,不是屏幕角度
测量面板除了距离与面积,还会给出两组方位读数:
| 读数 | 含义 | 出现条件 |
|---|---|---|
| Heading | 被测线段的方位,度数 + 16 方位英文缩写(如 310° NW) |
恒有 |
| Final heading | 到达终点的方位 | 只在跨到”收敛达到 1 度”的长线上出现 |
关键点在这句话(docs/user-guide/map-controls.md):
This is a true great-circle initial bearing, not the angle the line makes on screen: on a Mercator map Washington to London looks due east, while the real initial bearing is about 50°.
也就是说:在墨卡托地图上画一条”看起来正东”的线,量出来的 heading 可能是 50°。这不是 bug,而是因为墨卡托把大圆画成了曲线,屏幕上的直线是恒向线(rhumb line)。多点路径时,heading 取”首点到末点”的整体方向,不是逐段各给一行。
大圆在到达端会继续改变方向,所以终点方位与起点方位不同;只有短到方位变化不足 1° 的线才只显示一行。
9.7.5 Exact segment:键盘输入
Exact segment 是”不用点击、直接输入”的入口:填一个长度和一个方位角,点 Add,就从当前末端延伸出该尺寸的一段。它面向两类工作:
- 复述地契描述(”自某点向北 60° 东 120 米”);
- 导线测量(闭合导线、支导线)。
长度单位跟随面板的 Unit,方位角度制。
9.7.6 地形(3D)读数
开启 3D 地形后,面板多出一段 Terrain (3D):
| 行 | 含义 |
|---|---|
| Surface distance | 贴地距离(沿地形起伏) |
| Surface area | 贴地面积 |
| Gain / loss | 累计爬升 / 下降 |
| Min / max elevation | 沿线最低 / 最高高程 |
| Mean slope | 平均坡度 |
| 脚注 | Measured on: <body> (spherical approximation) |
这段在没有高程数据时自动隐藏;而 heading 那几行不会隐藏,因为方位是坐标上的纯几何,不需要 DEM。
提示:地形读数来自 MapLibre 的 terrain,而 terrain 只在 Web Mercator 下可用。切到自定义平面 CRS 后地形被关闭,这段读数会随之一并消失。
9.7.7 半径是怎么注入的
测量控件(maplibre-gl-components 的 MeasureControl)默认按地球半径算。GeoLibre 在创建时把当前天体的平均半径塞进去,并在后续切换天体时更新:
// packages/plugins/src/plugins/components/measure.ts(语义摘要)
const control = new MeasureControlClass({
...MEASURE_OPTIONS,
radius: getActiveMeanRadiusMeters(), // 按项目天体初始化
});
// 订阅 store:天体变了就 setRadius,而不是让控件一直用地球半径
control.setRadius(meanRadiusMeters(getEllipsoid(id)));
这里有一个真实的取舍:MeasureControl 用radius 把经纬度角度换成米,所以月球上的同一个多边形,面积会按 (1737.4/6371.0)² ≈ 7.4% 缩小——这正是它该有的样子。
9.7.8 属性表里的几何度量
同一条数学链在属性表的字段计算器里也能用,函数是 $length(unit)、$perimeter(unit)、$area(unit):
round($area("square-kilometers"), 2)
$length("kilometers")
$perimeter("meters")
实现要点(geometry-measure.ts):
- 距离用相邻顶点的 haversine 累加;
- 面积用球面盈余公式,外环减内环,取绝对值;
- 半径来自
getActiveMeanRadiusMeters(),所以和测量面板一致; - 跨越日界线的边按短边处理(经差归一化到 [−180, 180)),一份跨 179°→−179° 的多边形不会被算成绕地球一圈;
- 单位写错(例如
$area("hectare")少了 s)会抛错并把该行写成 null,同时在消息里报出错行数——而不是静默按平方米返回。
9.8 行星椭球与地外天体测量
9.8.1 12 个内置天体
ELLIPSOIDS(packages/core/src/ellipsoids.ts):
| id | 名称 | 长半轴 a(m) | 扁率倒数 1/f | 平均半径(m) |
|---|---|---|---|---|
earth |
Earth (WGS 84) | 6 378 137 | 298.257223563 | 6 371 008.77 |
moon |
Moon | 1 737 400 | 0(正球) | 1 737 400 |
mars |
Mars (IAU 2000) | 3 396 190 | 169.894447 | 3 389 526.67 |
mercury |
Mercury | 2 439 400 | 0 | 2 439 400 |
venus |
Venus | 6 051 800 | 0 | 6 051 800 |
io |
Io | 1 821 600 | 0 | 1 821 600 |
europa |
Europa | 1 560 800 | 0 | 1 560 800 |
ganymede |
Ganymede | 2 631 200 | 0 | 2 631 200 |
callisto |
Callisto | 2 410 300 | 0 | 2 410 300 |
titan |
Titan | 2 574 730 | 0 | 2 574 730 |
pluto |
Pluto | 1 188 300 | 0 | 1 188 300 |
charon |
Charon | 606 000 | 0 | 606 000 |
平均半径用 R = (2a + b) / 3,其中 b = a(1 − f)。地球代入得 (2 × 6378137 + 6356752.3142) / 3 = 6371008.77 m。这个值也是 EARTH_MEAN_RADIUS_METERS 常量——Turf.js 与 MapLibre 内部烘焙的那个半径。
9.8.2 球面近似与”地球锁定”的现实
GeoLibre 的所有量算最终都归结为”把角度乘以半径“。而 Turf.js 把地球半径写死了,没有逐调用覆盖的参数(这正是 issue #1128 里 “Turf is Earth-locked” 的那一半)。仓库选择的解法很直接:事后按比例缩放,而不是替换 Turf。
export function getActiveBodyRadiusRatio() {
return getActiveMeanRadiusMeters() / EARTH_MEAN_RADIUS_METERS;
}
// Turf 返回的长度 → 该天体真实长度;要传给 Turf 的距离 → 地球等效
export const earthLengthToBody = (length) => length * getActiveBodyRadiusRatio();
export const bodyLengthToEarth = (length) => length / getActiveBodyRadiusRatio();
// 面积按半径比的平方
export const earthAreaToBody = (area) => area * getActiveBodyRadiusRatio() ** 2;
这套换算对球体是精确的(GeoLibre 的非地球天体全部按球建模),对火星最差也只差 0.6% 的扁率。文件的注释也把出处写清了:这是 USGS Astrogeology 的 @thareUSGS 在 issue 里建议的做法。
警告:缩放是无量纲的。传进去什么单位,出来就是什么单位——没有隐式换算。如果一处代码先用
kilometers调 Turf、再用earthLengthToBody,结果仍然是千米。
9.8.3 天体与底图的配对
切换天体有两条路径,都会连带设置 ellipsoidId:
- Settings → Map Preferences → Celestial body:直接选天体;
- Layers 面板的星球切换器(
PLANET_SWITCHER_OPTIONS):选天体的同时套上配套底图。
切换器里的 12 个选项与配套底图:
| 天体 | 配套底图 |
|---|---|
| Earth | USGS Imagery(The National Map,全局 Web Mercator XYZ) |
| Moon | Hillshaded Albedo(OpenPlanetaryMap) |
| Mars | Viking MDIM2.1(OpenPlanetaryMap) |
| Callisto / Charon / Europa / Ganymede / Io | Galileo/Voyager 或 New Horizons 马赛克 |
| Mercury | MESSENGER Colour Mosaic |
| Pluto | New Horizons Mosaic |
| Titan | Cassini ISS Mosaic |
| Venus | Magellan C3-MDIR Colour |
底图分组的规则是:月球、火星各占一个分组,其余天体合并进 “Other celestial bodies”(按天体名字排序),地球的底图不进这个选择器——它在 OpenFreeMap/Protomaps 那一组里。
提示:这些行星底图的
styleUrl是geolibre://basemap/<id>形式的哨兵值,不是可请求的 URL,地图控制器在应用时才把它展开成栅格样式。项目里保存旧 id(mars-opm、moon-lroc等)时,LEGACY_PLANETARY_BASEMAP_IDS会映射到当前最近的一条,不会掉回地球底图。
9.8.4 行星测量的四个限制
- 只支持球面近似——UI 上直接写着
(spherical approximation); - 火星的扁率被忽略——按平均半径的球算,最差 0.6%;
- Elev 不工作——高程回退到的 Open-Meteo 服务只在球上跑,”never on a planetary basemap”;
- 地形不工作——3D terrain 是地球的 Web Mercator 高程网格。
9.9 高程读数、全球 DEM 与地形
9.9.1 打开 Elevation
Controls → Elevation 打开状态栏的 Elev 读数。它默认关闭(showPointerElevation: false),原因写在类型注释里:
with 3D terrain off the lookup falls back to the public Open-Meteo service, so hovering would send coordinates off the device for a readout the user never asked for
9.9.2 两级数据源
| 优先级 | 来源 | 触发条件 | 代价 |
|---|---|---|---|
| 1 | 地图自己的 3D 地形 | 有可用采样点 | 即时、随光标更新、不出设备 |
| 2 | Open-Meteo 高程 API | 地形没有该点的值(地形关着,或开着但该处无采样) | 指针静止 0.5 s 后发请求;按约 11 m 网格缓存;仅地球 |
第一次打开读数时会有一次同意询问。拒绝就保证永不出网:此时读数仍然在地形能回答的地方工作。请求在途时读数留空,而不是显示上一个点的高度——这个细节避免了”读到的是邻居的值”这类误判。
警告:开 3D 地形会让远程查询变少,但不会让它消失。要彻底断网,必须拒绝同意。
9.9.3 Eye alt
状态栏的 Eye alt 是相机海拔(Google Earth Pro 里同名)。两个特性:
- 按当前天体的比例换算,所以在月球上不会显示一个地球尺度的数字;
- 跟随
scaleUnit:metric(m/km)、imperial(ft/mi)、nautical(nmi)。
9.9.4 全球 DEM:Download Global DEM
Processing → GeoLibre Toolbox → Raster → Download Global DEM 从公开的 AWS Terrain Tiles 数据集下载 DEM(无需 API key),范围可取当前地图视图或在地图上拖框,然后在 Whitebox Toolbox 对话框里打开(不是 Raster 工具对话框)。
这一步的实际用途是给后面的地形/水文工具准备输入:下载得到的是 COG,可以直接喂给 Hillshade、Slope、Contour,也可以作为 3D 地形的自定义源。
9.9.5 地形源与夸张系数
| 引擎 | 地形源 | 夸张 |
|---|---|---|
| MapLibre | GeoLibre 的全局源,或兼容的本地/远程 COG DEM | 双击 Terrain 控件设置夸张 |
| ArcGIS | Esri World Elevation(无需 key),或本地/远程 COG DEM(EPSG:3857 或 4326) | Controls → Terrain exaggeration;Use global terrain 恢复 |
| Cesium | keyless Terrarium 高程,或配置的 COG;有 Ion token 时可用 Cesium World Terrain | — |
两个引擎的 DEM 语义差别值得注意:ArcGIS 的 COG 源里,缺失像素与 COG 之外的区域按 0 米处理,所以边界会出现”断崖”;MapLibre 侧不会。
9.9.6 高程与投影的交互
| 场景 | 高程/地形 | 原因 |
|---|---|---|
| Web Mercator 平面 / globe | ✅ | 原生 |
自定义平面 CRS(mapCrs) |
❌ 地形关闭 | 地形网格是 Web Mercator |
| ArcGIS 换投影的平面图 | ❌ 地形关闭 | local scene 不能用该投影 |
| 行星底图 | ❌ | 没有高程服务 |
| Cesium | ✅(其自身机制) | 独立高程 |
9.10 比例尺、缩放与地面分辨率
9.10.1 比例尺控件
Controls → Scale 显示比例尺条,单位由 Settings → Map Preferences → Scale bar units 决定:
scaleUnit |
单位 | 典型读数 |
|---|---|---|
metric |
m / km | 500 m、2 km |
imperial |
ft / mi | 2000 ft、1 mi |
nautical |
nmi | 2 nmi |
这个设置不止影响比例尺:状态栏的 Elev、Eye alt,以及右键 Quick analysis → Buffer … here 的三个预设(metric 下是 500 m / 1 km / 5 km,imperial 下是 0.25 mi / 1 mi / 5 mi)都跟着它走。
ArcGIS pane 的比例尺是 2D 图自己的实现:按视图中心、用当前天体的半径计算,所以月球工程上的比例尺是对的。
9.10.2 Web Mercator 的地面分辨率
EPSG:3857 把地球当成半径 R = 6378137 m 的球,世界范围是 [-20037508.34, 20037508.34] m 的正方形。在以 256 px 瓦片为约定的经典方案下,zoom 级 z 的赤道地面分辨率是:
resolution(z, lat) = 156543.03392804097 × cos(lat) / 2^z 单位:米/像素
其中 156543.03392804097 = 2πR / 256。注意 Web Mercator 的比例是”局部保角、整体严重失真”的:表格里的 cos(lat) 修正的是”这一像素在地面上对应多少米”,而不是”这一像素在投影平面上代表多少米”——后者恒为 156543.03 / 2^z,与纬度无关。
| zoom | 赤道分辨率(m/px) | 40°N 分辨率(m/px) | 一个 256 px 瓦片在赤道覆盖 |
|---|---|---|---|
| 0 | 156 543.03 | 119 918 | 40 075 km |
| 5 | 4 891.97 | 3 747.3 | 1 252 km |
| 8 | 611.50 | 468.4 | 156.5 km |
| 10 | 152.87 | 117.11 | 39.1 km |
| 12 | 38.22 | 29.28 | 9.78 km |
| 14 | 9.555 | 7.320 | 2.45 km |
| 16 | 2.389 | 1.830 | 611 m |
| 18 | 0.5972 | 0.4574 | 153 m |
| 20 | 0.1493 | 0.1144 | 38.2 m |
注意:MapLibre 的瓦片尺寸由样式决定(矢量瓦片常用 512 px)。同样数字的 zoom,在 512 px 方案下地面分辨率是上表的一半——这就是为什么不同底图在”同一个 zoom”看着疏密不同。判断尺度的可靠办法是看比例尺条或 Eye alt,不是看 zoom 数字。
9.10.3 天体上的尺度
在行星工程里,比例尺与 Eye alt 用该天体的半径换算,所以月球上一屏显示的公里数远小于地球。分辨率公式里的 R 也要换成天体的 semiMajorAxisMeters:
Mars: 2π × 3396190 / 256 ≈ 83 355 m/px (zoom 0,赤道)
Moon: 2π × 1737400 / 256 ≈ 42 642 m/px
这就是为什么”月球工程看着比地球工程缩放级别低”:数字 zoom 相同,覆盖的地面距离却差约 3.7 倍。
9.10.4 一个常见混淆
四个量互不替代:Zoom 无量纲、受引擎与瓦片尺寸影响;Eye alt 是相机相对海平面的高度,随天体半径与 scaleUnit 变;比例尺条量的是视图中心的地面距离,还受纬度影响;地面分辨率是 m/px,取决于 zoom、纬度、瓦片尺寸与天体。写报告用比例尺条或分辨率,做自动化用 zoom,做三维用 Eye alt。
9.11 实践建议与常见坑
9.11.1 选投影的决策表
| 你的场景 | 建议 | 理由 |
|---|---|---|
| 全球浏览、找地方 | Web Mercator + globe | 底图齐全、性能最好 |
| 中国境内的大比例尺量算 | 数据保持 4326,量算用 $area / 测量面板;需要平面精度时在 Processing 里转到 CGCS2000 3 度带 |
Web Mercator 在高纬/大范围面积失真极大 |
| 中国境内出图 | ArcGIS + 自定义 WKID(如 EPSG:4547)或导出后在 QGIS 排版 |
MapLibre 自定义 CRS 无底图 |
| 极地(南极科考、北极航道) | MapLibre earth-antarctic / earth-arctic |
有现成底图与网格 |
| 行星地质 | 对应行星预设 + 对应天体 | 预设会同时设置 ellipsoidId |
| 沿线剖面、通视 | Cesium 或 MapLibre + 3D terrain | 需要真实高程 |
| 需要”任意投影 + 底图” | ArcGIS(有 key 时)或外部 GIS | 只有 ArcGIS 能把底图一起换掉 |
9.11.2 十个常见坑
| # | 现象 | 原因 | 处理 |
|---|---|---|---|
| 1 | 图层落在 (0,0) 附近或贴着 ±180/±90 | 坐标是米制投影坐标却被当成度数;常见于缺 crs 的老式 GeoJSON、缺 .prj 的 Shapefile、纯坐标 CSV |
用 Vector → Reproject 填真实源 CRS,或在导入时手选 CRS |
| 2 | 整体偏移几十到几百米,形状正常 | 基准面缺 +towgs84:British National Grid 约 100 m,DHDN、Amersfoort、NAD27 同理 |
先查 datum-shift.ts 是否覆盖该代码;不覆盖就用 PROJ + 格网在外部先转 WGS84 |
| 3 | 缓冲区半径小了几万倍 | 客户端引擎(Turf.js)在经纬度上按度计算 | 换成 Sidecar (GeoPandas) 或 Python (Pyodide);或在距离字段旁的单位选择器里选米 |
| 4 | ST_Area 得到 0.0037 这样的数 |
EPSG:4326 下 ST_Area 返回平方度 |
ST_Area_Spheroid(ST_FlipCoordinates(geom)) / 1e6,或改用字段计算器的 $area |
| 5 | 切自定义 CRS 后一半图层消失、底图没了 | XYZ、矢量瓦片、MBTiles、Wayback 与共享底图都不在新 CRS 下绘制(9.5.3) | 属设计行为:改用 WMS/export 瓦片发布,或换 ArcGIS 引擎与它支持的投影 |
| 6 | 改了 mapCrs 地图毫无反应 |
① 主图不是 MapLibre;② 代码被 normalizeMapCrs 归一成默认;③ 代码不在随包 EPSG 表里;④ 只改了 ArcGIS pane |
按 9.4.5 / 9.4.4 逐条排查,确认改的是哪一个引擎的键 |
| 7 | 极区读数突然变成十进制度 | 纬度 > 84° 或 < −80° 时没有 UTM;MGRS 在极区走 UPS,mgrs 包不实现 |
极区改用 dd、dms,或用极射投影的 epsg 格式 |
| 8 | 看着很短的一段,量出 200 km | 高纬度墨卡托把东西向拉长;heading 又按大圆计算 | 相信测量面板的测地距离,不要用”屏幕像素 × 比例尺”估算 |
| 9 | 同一地点,UTM 带号与别的软件不一致 | 挪威 32V、斯瓦尔巴 31X/33X/35X/37X 的例外带,很多实现没有 | GeoLibre 的 UTM 与 MGRS 用同一套规则(Bergen 读 32V、Longyearbyen 读 33X),先确认对方是否实现例外 |
| 10 | 同形状多边形的面积只有地球的约 7.4% | 工程天体是月球,面积按 (R_moon/R_earth)² 缩放 |
不是错误;要对比就切换 ellipsoidId 并记录两次结果 |
坑 3 的对话框提示值得记住原文:geographicNote 直接写着”选中的图层使用地理坐标系(WGS84),所以距离单位是度,可在字段旁选米”。坑 6 的排查顺序按”命中率”排:先确认引擎,再看代码是否被归一,最后才是 EPSG 表覆盖范围——绝大多数”设了没反应”都出在第一步。
9.11.3 自查清单
- 图层 Metadata 里看到的源信息,是否和你的预期一致?
- 数据是经纬度还是米?看数值量级即可(经度 ≤ 180,东坐标通常 6–7 位)。
- 有没有
crs/.prj/geo元数据?没有就手选,别靠猜。 - 当前主图引擎是什么?它支持你要的投影吗?
- 量算用的单位与半径,跟你想的一致吗?
9.12 投影转换的入口
投影转换在 GeoLibre 里有六个入口,选错入口会让”为什么结果没变”变成一个长时间的谜。
| 入口 | 位置 | 参数 | 引擎 | 输出 |
|---|---|---|---|---|
| 导入时自动重投影 | 所有矢量导入路径 | 无(读文件自身元数据) | DuckDB ST_Transform |
WGS84 图层 |
| Reproject | Processing → GeoLibre Toolbox → Vector → Geometry → Reproject | Input layer、Source CRS(默认 EPSG:3857) |
仅 Sidecar / Pyodide | WGS84 新图层 |
| Reproject Vector | Processing → Projection → General → Reproject Vector | vector_in、dst_epsg 等 |
Whitebox(WASM 或 sidecar) | 目标 CRS 的文件/图层 |
| Assign Projection Vector | Processing → Vector → GeoLibre (WASM) → Assign Projection Vector |
vector_in、epsg |
Whitebox | 只写元数据,不改坐标 |
| Raster → Reproject | Processing → GeoLibre Toolbox → Raster → Reproject | 输入/输出路径、目标 CRS | 仅 sidecar(rasterio) | GeoTIFF |
| Raster → Resample | 同上 → Resample | 像素尺寸 | 仅 sidecar | GeoTIFF |
9.12.1 导入时自动重投影:99% 的情况应该走这条
只要格式自带 CRS(GeoParquet 的 geo 块、Shapefile 的 .prj、GPKG/GML/FGB 的 GDAL 元数据),导入就是全自动的,不需要任何参数。只有元数据缺失或不可信时才需要后面的手工工具。
9.12.2 Vector → Reproject:把”没有 CRS 的米制数据”救回来
这是唯一一个客户端引擎跑不了的矢量工具,代码里写得很明确:
该工具的定义里只有两个参数:layer(type: "layer",必填)与 source_crs(type: "string",必填,默认 EPSG:3857);requiresSidecar: true 让对话框把引擎默认选到 sidecar。客户端引擎执行时 run 只打一行日志,提示换引擎。
操作步骤:
- Processing → GeoLibre Toolbox → Vector;
- 在工具列表里选 Reproject;
Input layer选那个”位置不对”的图层;Source CRS填它真实的 CRS(不是目标 CRS——目标是固定的 WGS84);Engine选 Python (Pyodide)(浏览器可用,首次要下运行时)或 Sidecar (GeoPandas)(桌面);- Run,得到一个落在正确位置的新图层。
注意:这个工具做的是”重新解释 + 转换”,不是”改变声明”。如果原始坐标其实是对的、只是元数据缺失,用下面的 Assign Projection Vector 更合适——它只改声明,不重采样,不会因为你把源 CRS 填错而二次变形。
9.12.3 Whitebox 的投影族
Processing → Projection 下有 4 个工具:
| 工具 id | 名称 | 用途 |
|---|---|---|
reproject_vector |
Reproject Vector | 矢量转向目标 CRS(dst_epsg) |
reproject_lidar |
Reproject Lidar | 点云转 CRS |
georeference_raster_from_control_points |
Georeference Raster From Control Points | 用控制点给无坐标栅格赋地理参考 |
orthorectification |
Orthorectification | 正射校正 |
此外有两个”只改声明”的工具分散在别的类目里:
| 工具 id | 名称 | 位置 |
|---|---|---|
assign_projection_vector |
Assign Projection Vector | Processing → Vector → GeoLibre (WASM) |
assign_projection_raster |
Assign Projection Raster | Processing → Raster → GeoLibre (WASM) |
这两类工具的 epsg / dst_epsg 参数都会渲染成 9.3 的 CRS 选择器(Browse),不是裸数字框。
9.12.4 栅格:Reproject 与 Resample 是两件事
| 工具 | 改什么 | 不该用来做什么 |
|---|---|---|
| Reproject | 坐标系(重采样到新网格) | 改分辨率 |
| Resample | 像素尺寸(同一 CRS 内) | 改坐标系 |
两个都需要 rasterio sidecar。浏览器里可用的栅格工具只有八个(Hillshade、Slope、Aspect、Clip by extent、Raster calculator、Spectral index、Reclassify、Focal statistics),不含 Reproject。
9.12.5 Conversion 与云原生输出的 CRS
Processing → GeoLibre Toolbox → Conversion 的输出 CRS 行为分两种:
| 输出 | CRS 行为 |
|---|---|
| Vector to Vector(桌面 sidecar) | 可写任意 GDAL 格式;CRS 由写作时的参数决定 |
| 浏览器端各转换 | 写 GeoJSON、CSV、GeoParquet、GeoPackage、FlatGeobuf、Shapefile |
| Whitebox 工具的矢量输出 | 下拉里有 GeoJSON (add to map) / GeoParquet (download, keeps CRS) / FlatGeobuf / Shapefile;后三者保存到目标 CRS |
最后一条配着一句必须知道的话(对话框里的 projectedHint):
Saved to your downloads in the target CRS. The map only displays EPSG:4326, so a projected result is not added as a layer.
也就是说:Whitebox 里选了投影输出的矢量结果,会以文件形式下载而不上地图——因为地图只显示 4326。这不是缺陷,而是”不要在渲染层偷偷重投影”的一致选择。需要看结果就先转回 4326,或者用矢量工具而不是 Whitebox 工具。
9.12.6 SQL 里的 ST_Transform
SQL 工作区里可以显式转换:
SELECT * EXCLUDE (geom),
ST_Transform(geom, 'EPSG:32650', 'EPSG:4326') AS geom
FROM utm_layer;
ST_Transform 与 ST_Read、ST_AsGeoJSON 一样,属于 spatial 扩展(运行时单独加载的第二份 WASM),不属于 DuckDB 本体;具体能转哪些 CRS 取决于该构建带的 PROJ 数据。完整用法见 第11章 SQL 工作区与 DuckDB 空间分析。
9.13 本章小结
- GeoLibre 只有一套数据坐标:矢量恒为 EPSG:4326、经度在前;其余一切 CRS 都在外围(导入、渲染、量算)处理。
- 项目 CRS 是一组键:
mapCrs(MapLibre 平面)、arcgisWkid(ArcGIS 平面)、projection(globe/mercator)、ellipsoidId(天体)、coordinateFormat+coordinateEpsgCode(状态栏)、scaleUnit(单位制)。 - Web Mercator 是缺省不是硬编码:3857/900913/102100/102113 都被
normalizeMapCrs归一成”默认”,3413/3031/4326 会被归一成预设。 - 四套引擎四套机制:MapLibre 能换平面 CRS(瓦片不重投影);Mapbox 只有 Mercator/globe;Cesium 是场景(按地面分辨率同步);ArcGIS 能在平面上换任意 WKID(底图不画、COG 会 warp)。
- 18 个 MapLibre 预设:地球 3 个(GIBS 底图)、月球与火星 6 个(USGS WMS 底图)、其余 9 个天体全为简单圆柱;自定义 EPSG 无底图,网格按 2.5 倍尺度阈值拟合。
- 7 种坐标格式:dd/dms/ddm/utm/mgrs/usng/epsg;UTM 在 ±(80,84) 外、MGRS 在极区都回退到 dd,不显示假数字。
- 测量是大圆几何:heading 是大圆初始方位(华盛顿→伦敦约 50°,看着却像正东);Final heading 只在高纬度长线上出现。
- 半径决定一切量算:
getActiveMeanRadiusMeters()用一个单例贯穿测量面板、字段计算器与 Processing 工具;12 个天体、平均半径(2a+b)/3。 - 地球锁定用比例缩放绕开:
earthLengthToBody/bodyLengthToEarth/earthAreaToBody,面积按平方。 - 高程是两级来源:地形优先、Open-Meteo 兜底(需同意、0.5 s 防抖、约 11 m 缓存、仅地球);拒绝同意即永不出网。
- 比例尺、zoom、Eye alt、分辨率是四个量:分辨率
156543.03 × cos(lat) / 2^z(256 px 约定,512 px 瓦片减半),天体上按半径重算。 - 六个转换入口:导入自动、Vector→Reproject(仅 Pyodide/Sidecar)、Whitebox 投影族(
dst_epsg/epsg)、Assign Projection(只改声明)、Raster→Reproject/Resample(仅 sidecar)、SQLST_Transform。
9.14 实践练习
-
坐标格式全流程:加载任意一个点图层,把状态栏坐标依次切成
dd、dms、ddm、utm、mgrs、usng、epsg(EPSG 填32650),记录同一个点七种写法;再把地图平移到南纬 85°,观察utm与mgrs的回退,并解释为什么。 -
投影失配的诊断:取一份 EPSG:32650 的 GeoJSON,手工删掉顶层
crs成员后导入,记录图层落点;然后用 Processing → GeoLibre Toolbox → Vector → Reproject(Source CRS = EPSG:32650,引擎选 Pyodide)修复,比较两次的 Metadata 与坐标量级。最后用 Assign Projection Vector 做一次”只改声明”的对照,说明两者差异。 -
墨卡托的失真:在赤道与 60°N 各画一个 100 km × 100 km 的正方形(可用 Regular grid 或按米制缓冲构造),分别用
$area与ST_Area计算,比较结果并解释为什么”在地图上看起来一样大”的两个方块面积不同。 -
自定义平面 CRS:在 Settings → Map Preferences → Map coordinate reference system 里选 Custom EPSG code… → 3031,记录以下四件事:底图是否还在、XYZ 图层是否还在、Globe 按钮是否可点、地形是否还能开;再切回默认,复述
normalizeMapCrs对 3031 的处理(为什么它会变成预设而不是EPSG:3031)。 -
天体量算:把工程的 Celestial body 切到 Moon,量一个已知边长的多边形面积,再切回 Earth 复量,计算两次的比值,并与
(1737400 / 6371008.77)²对比;说明为什么这个比值与形状无关。 -
高程读数与地形:关闭 3D 地形时打开 Controls → Elevation 并拒绝远程查询同意,记录哪些位置仍有读数;再打开地形重复一次,并对比 Elev 与 Eye alt 在同一视角下的差异。
延伸阅读
- 仓库文档:
docs/user-guide/map-controls.md、docs/user-guide/interface.md(Coordinate format / Elevation readout)、docs/user-guide/settings.md(Map Preferences)、docs/maplibre-renderer.md(Other coordinate reference systems)、docs/arcgis-renderer.md(Map projections) - 源码:
packages/core/src/{map-crs,ellipsoids,datum-shift,types}.ts、packages/map/src/epsg-proj4.ts、apps/geolibre-desktop/src/lib/{crs-catalog,crs-utils,coordinate-format,dms,geometry-measure,processing-params}.ts、packages/plugins/src/plugins/components/measure.ts - 测试:
tests/crs-catalog.test.ts、tests/coordinate-format.test.ts、tests/dms.test.ts、tests/geometry-measure.test.ts、tests/measure-heading.test.ts - 相关章节:第05章 数据添加与格式支持、第06章 图层管理与地图控制、第08章 渲染引擎与三维可视化、第11章 SQL 工作区与 DuckDB 空间分析