第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 在几个边界上做了归一:

  1. 读 EPSG 定义时,epsg-proj4.ts 会把 proj4 定义串里的 +axis=... 剥掉(raw.replace(/\+axis=\w+\s*/g, "")),并单独记录 northFirst 标志,让转换器始终按东-北顺序工作;
  2. WMS 1.1.1 的 BBOX 恒为东、北顺序,所以自定义 CRS 的 WMS 请求一律走 1.1.1 而不是 1.3.0(见 9.5.4);
  3. 行星预设的 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;
}
  1. resolveEpsgProjection(packages/map/src/epsg-proj4.ts)——把 EPSG 代码解析成 proj4 定义时;
  2. withGeoKeysDatumShift——从 GeoTIFF 的 geokeys 建立 COG 源 CRS 时(installCogTilerDatumShift 把它挂到 cog-tiler-wasm 的 setSourceCrsResolver 上);
  3. 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;
}

四条推论:

  1. 900913、102100、102113 是 3857 的历史别名(Google、Esri 各一套),写进项目文件也不会让地图变成”自定义 CRS”;
  2. EPSG:3413、3031、4326 三个代码会被归一到对应预设 id,因为它们各自带底图;
  3. 其它任何代码 → "EPSG:<code>",此时地图进入”无底图的自定义平面 CRS”模式(9.5.2);
  4. 值畸形(非字符串、EPSG:abc)→ 当作缺省,不做任何事,也不会报错。

9.2.4 导入 → 渲染的完整链路

把前面几节串起来,一份”坐标是 EPSG:27700 的 Shapefile”进到地图上,实际发生了五步:

  1. 读取器拿到文件,从 .prj 读出 EPSG:27700;若声明是 4326(或 3857 带 z),走”便宜路径”,否则走重投影;
  2. reprojectFeatureCollectionToWgs84 用 ST_Transform(spatial 扩展,proj4/PROJ 提供数学)把每个坐标转成经纬度;
  3. epsg-proj4.ts 在解析 27700 时给定义补上 OSGB 1936 的 +towgs84(9.1.4),否则整体偏约 100 m;
  4. 结果作为 FeatureCollection 交给 addGeoJsonLayer,图层里存的是经纬度;
  5. 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) 的行为可以归纳成五条:

  1. 空白查询返回整张表,保持精选顺序(limit 默认等于表长,所以默认列表不会被”截断在族中间”);
  2. 忽略 EPSG 前缀(冒号、空格可选),因此 epsg:32617、EPSG 32617、EPSG32617、32617 等价;
  3. 按空白分词,每个词都要命中(名字或代码),所以 utm 17n 收窄到 zone 17 的北半球系;
  4. 排序分四档:精确代码 > 代码前缀 > 名称前缀 > 名称包含;同档内保持目录顺序;
  5. 不认识的词就让结果为空,不做模糊纠错。

几个可以直接对照的结果:

查询 结果特征
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” 小节):

  1. 矢量图层(GeoJSON、要素服务、符号化包生成的伴生图层)由 SDK 的 projectOperator 在客户端逐要素重投影;
  2. COG 会被 warp 到该投影(arcgis-reprojected-tiles.ts):视图投影分块,每块用 33×33 的采样格点把 Web Mercator 瓦片的内容插值填进来;跨投影接缝(Spilhaus 会把陆地切开)的格子逐像素投影;Web Mercator 止于 ±85.05°,所以极冠是空的;
  3. 底图不画:Esri 底图样式与 XYZ 瓦片不会被 warp,只有 Blank 背景色;
  4. 相机、指针坐标、识别、选择、范围绘制都在投影与经纬度之间换算,所以共享相机、其它 pane、保存的视图仍然是度数;
  5. 即使开了地形,平面图仍然是 MapView(local scene 不能用该投影);
  6. 未知 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 不重投影
共享底图 ❌ 不画 同上

同时有三条”顺带关闭”:

  1. Globe 按钮隐藏——平面 CRS 下没有球面;
  2. 地形关闭——地形是 Web Mercator 的高程网格;
  3. 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 切换与保存

三条等价路径:

  1. 点击状态栏的坐标文本——循环到下一个格式(nextCoordinateFormat,顺序就是上表的顺序,epsg 之后回到 dd);
  2. Settings → Map Preferences → Coordinate format 直接选;
  3. 选 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 操作流程

  1. Controls → Measure;
  2. 选 Distance / Area / Circle;
  3. 在 Unit 里选单位(见 9.7.3);
  4. 点 Start;
  5. 在地图上逐点点击加点;
  6. 双击或右键结束当前测量;
  7. 继续下一条,或点 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):

  1. 距离用相邻顶点的 haversine 累加;
  2. 面积用球面盈余公式,外环减内环,取绝对值;
  3. 半径来自 getActiveMeanRadiusMeters(),所以和测量面板一致;
  4. 跨越日界线的边按短边处理(经差归一化到 [−180, 180)),一份跨 179°→−179° 的多边形不会被算成绕地球一圈;
  5. 单位写错(例如 $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:

  1. Settings → Map Preferences → Celestial body:直接选天体;
  2. 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 行星测量的四个限制

  1. 只支持球面近似——UI 上直接写着 (spherical approximation);
  2. 火星的扁率被忽略——按平均半径的球算,最差 0.6%;
  3. Elev 不工作——高程回退到的 Open-Meteo 服务只在球上跑,”never on a planetary basemap”;
  4. 地形不工作——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 自查清单

  1. 图层 Metadata 里看到的源信息,是否和你的预期一致?
  2. 数据是经纬度还是米?看数值量级即可(经度 ≤ 180,东坐标通常 6–7 位)。
  3. 有没有 crs/.prj/geo 元数据?没有就手选,别靠猜。
  4. 当前主图引擎是什么?它支持你要的投影吗?
  5. 量算用的单位与半径,跟你想的一致吗?

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 只打一行日志,提示换引擎。

操作步骤:

  1. Processing → GeoLibre Toolbox → Vector;
  2. 在工具列表里选 Reproject;
  3. Input layer 选那个”位置不对”的图层;
  4. Source CRS 填它真实的 CRS(不是目标 CRS——目标是固定的 WGS84);
  5. Engine 选 Python (Pyodide)(浏览器可用,首次要下运行时)或 Sidecar (GeoPandas)(桌面);
  6. 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 本章小结

  1. GeoLibre 只有一套数据坐标:矢量恒为 EPSG:4326、经度在前;其余一切 CRS 都在外围(导入、渲染、量算)处理。
  2. 项目 CRS 是一组键:mapCrs(MapLibre 平面)、arcgisWkid(ArcGIS 平面)、projection(globe/mercator)、ellipsoidId(天体)、coordinateFormat+coordinateEpsgCode(状态栏)、scaleUnit(单位制)。
  3. Web Mercator 是缺省不是硬编码:3857/900913/102100/102113 都被 normalizeMapCrs 归一成”默认”,3413/3031/4326 会被归一成预设。
  4. 四套引擎四套机制:MapLibre 能换平面 CRS(瓦片不重投影);Mapbox 只有 Mercator/globe;Cesium 是场景(按地面分辨率同步);ArcGIS 能在平面上换任意 WKID(底图不画、COG 会 warp)。
  5. 18 个 MapLibre 预设:地球 3 个(GIBS 底图)、月球与火星 6 个(USGS WMS 底图)、其余 9 个天体全为简单圆柱;自定义 EPSG 无底图,网格按 2.5 倍尺度阈值拟合。
  6. 7 种坐标格式:dd/dms/ddm/utm/mgrs/usng/epsg;UTM 在 ±(80,84) 外、MGRS 在极区都回退到 dd,不显示假数字。
  7. 测量是大圆几何:heading 是大圆初始方位(华盛顿→伦敦约 50°,看着却像正东);Final heading 只在高纬度长线上出现。
  8. 半径决定一切量算:getActiveMeanRadiusMeters() 用一个单例贯穿测量面板、字段计算器与 Processing 工具;12 个天体、平均半径 (2a+b)/3。
  9. 地球锁定用比例缩放绕开:earthLengthToBody / bodyLengthToEarth / earthAreaToBody,面积按平方。
  10. 高程是两级来源:地形优先、Open-Meteo 兜底(需同意、0.5 s 防抖、约 11 m 缓存、仅地球);拒绝同意即永不出网。
  11. 比例尺、zoom、Eye alt、分辨率是四个量:分辨率 156543.03 × cos(lat) / 2^z(256 px 约定,512 px 瓦片减半),天体上按半径重算。
  12. 六个转换入口:导入自动、Vector→Reproject(仅 Pyodide/Sidecar)、Whitebox 投影族(dst_epsg/epsg)、Assign Projection(只改声明)、Raster→Reproject/Resample(仅 sidecar)、SQL ST_Transform。

9.14 实践练习

  1. 坐标格式全流程:加载任意一个点图层,把状态栏坐标依次切成 dd、dms、ddm、utm、mgrs、usng、epsg(EPSG 填 32650),记录同一个点七种写法;再把地图平移到南纬 85°,观察 utm 与 mgrs 的回退,并解释为什么。

  2. 投影失配的诊断:取一份 EPSG:32650 的 GeoJSON,手工删掉顶层 crs 成员后导入,记录图层落点;然后用 Processing → GeoLibre Toolbox → Vector → Reproject(Source CRS = EPSG:32650,引擎选 Pyodide)修复,比较两次的 Metadata 与坐标量级。最后用 Assign Projection Vector 做一次”只改声明”的对照,说明两者差异。

  3. 墨卡托的失真:在赤道与 60°N 各画一个 100 km × 100 km 的正方形(可用 Regular grid 或按米制缓冲构造),分别用 $area 与 ST_Area 计算,比较结果并解释为什么”在地图上看起来一样大”的两个方块面积不同。

  4. 自定义平面 CRS:在 Settings → Map Preferences → Map coordinate reference system 里选 Custom EPSG code… → 3031,记录以下四件事:底图是否还在、XYZ 图层是否还在、Globe 按钮是否可点、地形是否还能开;再切回默认,复述 normalizeMapCrs 对 3031 的处理(为什么它会变成预设而不是 EPSG:3031)。

  5. 天体量算:把工程的 Celestial body 切到 Moon,量一个已知边长的多边形面积,再切回 Earth 复量,计算两次的比值,并与 (1737400 / 6371008.77)² 对比;说明为什么这个比值与形状无关。

  6. 高程读数与地形:关闭 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 空间分析