投影与坐标系
坐标问题是 OpenLayers 上手阶段唯一真正的门槛。 地图、要素、服务、鼠标事件四处的坐标必须统一在同一个投影下,否则就会出现偏移、飞到海里、只显示半个世界。
默认坐标系不是经纬度
OpenLayers 的 View 默认使用 EPSG:3857,也就是 Web 墨卡托,
坐标单位是米。它的世界范围大致是 [-20037508, -20037508, 20037508, 20037508],
原点在本初子午线与赤道的交点,向东、向北为正。
GIS 业务数据绝大多数是 EPSG:4326,单位是度,范围是经度 ±180、纬度 ±90。
把 [116.39, 39.90] 直接交给 EPSG:3857 的视图,
等于告诉地图“往东走 116 米、往北走 39 米”,于是视角落在几内亚湾附近,
屏幕上只剩一片海洋。
判断方法:如果地图打开后中心在非洲西海岸外,或者缩到 0 级看到整个世界, 基本可以确定是经纬度没转换。
两个转换函数
一对函数解决 90% 的问题,方向不要记反:
// 经纬度 -> 米(给 View 的 center、Feature 的 geometry 用)
var center = ol.proj.fromLonLat([116.391263, 39.907359]);
// 米 -> 经纬度(把事件里的坐标回显给用户时用)
var lonlat = ol.proj.toLonLat(evt.coordinate);
函数内部判断当前视图投影,默认就是 EPSG:3857。
如果视图改成了别的投影,要显式传第二个参数:
var center = ol.proj.fromLonLat([116.39, 39.90], 'EPSG:4490');
三处坐标,别混用
| 位置 | 坐标含义 | 来源 |
|---|---|---|
view.getCenter() | 视图投影下的米 | 创建时用 fromLonLat 转换 |
feature.getGeometry().getCoordinates() | 要素所在数据源的投影 | 读取 GeoJSON 时指定 featureProjection |
evt.coordinate | 视图投影下的米 | 回显前用 toLonLat 转换 |
map.getCoordinateFromPixel() 返回的同样是视图投影下的米制坐标,
常有人拿它当经纬度存库,之后所有空间查询都偏出几百公里。
读取数据时的投影声明
矢量数据本身带有自己的坐标系。用格式解析器读取时, 必须同时告诉它“数据是什么投影”和“我要什么投影”, OpenLayers 会自动逐点重投影:
var format = new ol.format.GeoJSON();
var features = format.readFeatures(geojson, {
dataProjection: 'EPSG:4326', // 文件里是经纬度
featureProjection: 'EPSG:3857' // 地图视图用墨卡托
});
漏掉 featureProjection 的典型后果是:地图上什么也看不到,
因为要素坐标落在 [116, 39] 这一小片区域里,
而当前视野在几千万米之外。用 view.fit(source.getExtent()) 倒是能看到,
但整张图会挤成一个点。
数据源级别的声明:如果是手工 addFeature,
可以在 ol.source.Vector 上写 projection: 'EPSG:4326',
但更稳妥的做法是在构造几何体时就转好,避免后续量算单位混乱。
视图切到别的投影
View 的 projection 参数决定地图的坐标参考。
改成 EPSG:4326 后,地图容器里的坐标直接用经纬度,
代价是墨卡托的等角特性消失,高纬度地区变形明显,瓦片也要能接受 4326 请求。
var view = new ol.View({
projection: 'EPSG:4326',
center: [116.391263, 39.907359], // 此时不需要 fromLonLat
zoom: 8
});
更换投影后,fromLonLat 的目标投影也跟着变,
所以这段代码里不能再套一层转换,否则会得到双重投影的坐标。
注册国内常用投影
政务数据常见 EPSG:4490(CGCS2000 经纬度)、
EPSG:4547(CGCS2000 三度带高斯投影)。
OpenLayers 只内置了少数几个,其余要借助 proj4 注册:
<script src="https://cdn.jsdelivr.net/npm/proj4@2.12.1/dist/proj4.js"></script>
<script>
proj4.defs('EPSG:4547',
'+proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 ' +
'+ellps=GRS80 +units=m +no_defs');
ol.proj.proj4.register(proj4);
var view = new ol.View({
projection: 'EPSG:4547',
center: [500000, 4420000],
zoom: 10
});
</script>
注册之后再调用 ol.proj.fromLonLat([116.39, 39.90], 'EPSG:4547')
就能拿到平面坐标,图层与视图的投影不一致时也会自动重投影。
分辨率和缩放级别
在 EPSG:3857 下,0 级一张 256 像素的瓦片覆盖整个赤道周长
40075016.686 米,所以某一级的分辨率是:
resolution = 40075016.686 / (256 * Math.pow(2, zoom));
这个值表示“一个屏幕像素代表多少米”,常用于按比例设置线宽、
判断当前是否该显示某个图层。代码里可以直接读:
map.getView().getResolution()。
距离和面积用球面公式
墨卡托投影会放大高纬度的距离,用平面勾股算出来的长度并不可信。 OpenLayers 提供了基于 WGS84 椭球的计算函数:
var length = ol.sphere.getLength(lineGeometry); // 米
var area = ol.sphere.getArea(polygonGeometry); // 平方米
这两个函数接受任意投影的几何体,内部会先转成经纬度再计算,
所以不必手动 toLonLat。
坐标问题排查表
| 症状 | 原因 | 修正 |
|---|---|---|
| 打开后停在海面上 | 经纬度直接当米用 | ol.proj.fromLonLat() |
| 要素全挤在一个点 | 未指定 featureProjection | 读取时补上目标投影 |
| 点位整体偏移几百米 | 数据是 GCJ-02 或地方坐标系 | 先用转换库纠偏再交给地图 |
| WMS 图与原底图错位 | 服务端 CRS 与视图投影不一致 | 统一 projection 或改用 4326 视图 |
| 量算距离偏大 | 用平面坐标算欧氏距离 | 改用 ol.sphere.getLength |
排查时最有效的一步是打印三个值:视图投影、要素第一个坐标、
以及 ol.proj.toLonLat 之后的经纬度。
三者对不上,问题一定在投影环节。
经纬志(2026)。《投影与坐标系》。https://www.jingweizhi.com/openlayers/tutorial/projection.html(访问日期 2026-11-03)。