前端地图基本原理

1. 基本概念

1-1. 经纬度的描述

地球是一个椭球,Datum 是一组用于描述这个椭球的数据集合。最常用的一个 Datum 是 WGS84 (World Geodetic System 1984),它的主要参数有:

  • 坐标系的原点是地球质心(center of mass)
  • 子午线(meridian),即零度经线,位于格林威治子午线 Royal Observatory 所在纬度往东 102.5米 所对应的的经线圈
  • 椭球截面长轴为 a=6378137米
  • 椭圆截面短轴为 b=6356752.3142米,可选参数
  • 扁平比例(flattening)f=(a−b)/a=1/298.257223563
  • geoid,即海平面,用于定义高度

1-2. 像素坐标系

像素坐标系,也可以成为屏幕坐标系,像素坐标系和地图的经纬度坐标系存在对应关系,屏幕上的每一个像素都对应一个经纬度点位置。 不同缩放级别下,像素坐标系和经纬度坐标系的对应关系是不同的。

1-3. 投影

地图投影是利用一定数学法则把地球表面的经、纬线转换到平面上的理论和方法。由于地球是一个赤道略宽两极略扁的不规则的梨形球体,故其表面是一个不可展平的曲面,所以运用任何数学方法进行这种转换都会产生误差和变形,为按照不同的需求缩小误差,就产生了各种投影方法。

map_projection

关于投影我们只需要了解墨卡托投影(正轴等角圆柱投影)

1-4. 墨卡托投影

墨卡托投影,是正轴等角圆柱投影。由荷兰地图学家墨卡托 (G.Mercator) 于 1569 年创立。假想一个与地轴方向一致的圆柱切或割于地球,按等角条件,将经纬网投影到圆柱面上,将圆柱面展为平面后,即得本投影。等角条件是使地球面上微分区域内两个方向的夹角投影到平面以后,保持角度不变的条件。

Web 墨卡托投影(又称球体墨卡托投影)是墨卡托投影的变种,它接收的输入是 Datum 为 WGS84 的经纬度,但在投影时不再把地球当做椭球而当做半径为 6378137 米的标准球体,以简化计算。

mercator_projection

Web 墨卡托投影的两个投影标准:

  • EPSG4326:Web 墨卡托投影后的平面地图,但仍然使用 WGS84 的经度、纬度表示坐标;
  • EPSG3857:Web 墨卡托投影后的平面地图,坐标单位为米。

这两个投影标准在我们调用 geoserver 的服务时可能会用到,所以简单了解即可。

1-5. 瓦片

经过投影后,地图就变为平面的一张地图。考虑到有时候我们需要看宏观的地图信息(如世界地图里每个国家的国界),有时候又要看很微观的地图信息(如导航时道路的路况信息)。为此,我们对这张地图进行等级切分。在最高级 (zoom=0),需要的信息最少,只需保留最重要的宏观信息,因此用一张 256x256 像素的图片表示即可;在下一级 (zoom=1),信息量变多,用一张 512x512 像素的图片表示;以此类推,级别越低的像素越高,下一级的像素是当前级的4倍。这样从最高层级往下到最低层级就形成了一个金字塔坐标体系。

对每张图片,我们将其切分为 256x256 的图片,称为瓦片(Tile)。这样,在最高级 (zoom=0) 时,只有一个瓦片;在下一级 (zoom=1) 时有4个瓦片;在下一级 (zoom=2) 时有16个瓦片,以此类推。

conversion_of_coordinates

1-6. 瓦片的编号

瓦片生成后,就是一堆图片。怎么对这堆图片进行编号,是目前主流互联网地图商分歧最大的地方。总结起来分为四个流派:

  • 谷歌 XYZ:Z 表示缩放层级,Z=zoom;XY 的原点在左上角,X 从左向右,Y 从上向下。
  • TMS:开源产品的标准,Z 的定义与谷歌相同;XY 的原点在左下角,X 从左向右,Y 从下向上。
  • QuadTree:微软 Bing 地图使用的编码规范,Z 的定义与谷歌相同,同一层级的瓦片不用 XY 两个维度表示,而只用一个整数表示,该整数服从四叉树编码规则
  • 百度 XYZ:Z 从 1 开始,在最高级就把地图分为四块瓦片;XY 的原点在经度为 0 纬度位 0 的位置,X 从左向右,Y 从下向上

tile_coding

2. Geoserver

先看一段官方的介绍:

GeoServer 是基于 Java 的软件服务器,允许用户查看和编辑地理空间数据。使用开放地理空间联盟(OGC)提出的开放标准,GeoServer 在地图创建和数据共享方面具有极大的灵活性。
GeoServer 允许您向世界显示您的空间信息。实施Web地图服务 (WMS) 标准,GeoServer 可以创建各种输出格式的地图。一个免费的地图库 OpenLayers 已集成到 GeoServer 中,从而使地图生成快速简便。
GeoServer 符合 Web Feature Service(WFS)标准和 Web Coverage Service(WCS)标准,该标准允许共享和编辑用于生成地图的数据。GeoServer 还使用 Web Map Tile Service 标准将您发布的地图拆分为图块,以方便Web地图和移动应用程序使用。

GeoServer 使用的是图层与图层组的概念。将在服务器上准备发布为服务的数据定义为一组数据集,然后规定在发布为Web服务时的一些参数。Geoserve 提供了操作界面来管理各种配置:

geoserver_page

3. leaflet 和 openlayers

上文说到的 openlayersleaflet 都是用于在 Web 上创建交互式地图,可以显示从任何来源加载的地图图块、矢量数据和标记。

这里的两者都是开源的,相比起来 leaflet 更加轻量,适合移动端的场景,如果只是相对简单的地图需求,使用 leaflet 会是一个合适的选择。而 openlayers 具有数量更大,更复杂的 API。而 geoserver 提供的服务,对 leaflet 和 openlayers 都是支持的。

4. 调用 geoserver 的服务

4-1. 调用 WMS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const wmsLayer = Map.tileLayer.wms(
'http://gsmap.sf-express.com/geoserver/sfmap/wms',
{
type: 'tile',
layers: 'map:example_layer',
version: '1.1.1',
format: 'image/png',
request: 'GetMap',
transparent: 'true',
zoom: [3, 20],
zIndex: 5,
sld_body: getSld('sfmap:dept_3', this.cateMapData),
},
)

this.mapIns.addLayer(wmsLayer)

此处使用 Map.tileLayer.wms(url, params) 调用来自 geoserver 的 WMS 服务,使用的参数如下:

  • url:请求的 geoserver 服务地址
  • params:请求的目标图层相关配置参数,layers 为请求的图层名,在 geoserver 服务中不同维度的地图图层对应不同的名字,在这里的 dept_3 表示业务区的地图图层;request 表示此处调用的是 geoserver 的获取地图图层的服务;formattranparent表示请求的瓦片图文件格式及背景是否透明; 而 sld_body 则是需要自定义的 SLD 样式

4-2. 配置 SLD

对于 WMS 返回的地图图层,可以针对之进行样式的自定义,geoserver 支持多种方法定义图层样式,SLD 就是其中一种。

SLD (Style Layer Descriptor) 是2005年OGC提出的一个标准,这个标准在一定条件下允许WMS服务器对地图可视化的表现形式进行扩展。该 SLD 规范是采用XML定义地图显示样式,通过自定义SLD来配置地图图层渲染的可视化风格,可以设置过滤器,自定义图例等。

SLD_structure

如果我们想要自定义图层的样式,肯定会用到的元素包括:

  • FeatureTypeStyle:这一部分是整个样式文档的根节点,并说明什么是它的样式将被应用的特征类型。FeatureTypeStyle 包含一个或者多个 Rule 元素,Rule 元素允许有条件的映射。

  • RULE(规则):规则是根据属性条件和地图比例尺来对要素进行分组渲染,一般 RULE 中只允许渲染一种类型的要素,即点,线,面等其中的一种,但是可以和注记同时使用。

  • Symbolizer(符号):Symbolizer 指定数据应该如何可视化,在1.0的标准中包含五忠类型的Symbolizer,分别是PointSymbolizer(点符号)、LineSymbolizer(线符号)、PloygonSymbolizer(面符号)、TextSymbolizer(注记)、RasterSymbolizer(栅格)。

除了一些预设的匹配规则,SLD 还提供了一些 Function 可应用于较为负责的筛选。

以实际应用举例,见以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Rule>
<ogc:Filter>
<ogc:PropertyIsEqualTo>
<ogc:Function name="in">
<ogc:PropertyName>code</ogc:PropertyName>
<ogc:Literal>591Y</ogc:Literal>
<ogc:Literal>592Y</ogc:Literal>
<ogc:Literal>595Y</ogc:Literal>
<ogc:Literal>791Y</ogc:Literal>
</ogc:Function>
<ogc:Literal>true</ogc:Literal>
</ogc:PropertyIsEqualTo>
</ogc:Filter>
<PolygonSymbolizer>
<Fill>
<CssParameter name="fill">#7ba7ee</CssParameter>
<CssParameter name="fill-opacity">0.7</CssParameter>
</Fill>
<Stroke>
<CssParameter name="stroke">#727D71</CssParameter>
<CssParameter name="stroke-width">0.4</CssParameter>
</Stroke>
</PolygonSymbolizer>
</Rule>

这段 SLD 表示对数据对象进行筛选匹配,当对象的 code 属性为 591Y、592Y、595Y 和 791Y 四者的其一时,适用后续的样式。后续的样式表示对应的 PolygonSymbolizer 元素适用了 Fill 和 Stroke 的样式,具体的样式属性分别是填充颜色、填充透明度、边界颜色和边界宽度。

这里需要留意的是任何 Function 元素都会有一个返回值, <ogc:Function name="in"></ogc:Function> 对应的返回值是一个 Boolean 值,所以需要在后面紧跟 <ogc:Literal>true</ogc:Literal> 来对应返回值的匹配。

4-3. 获取点击位置所属业务区

还是看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 监听地图的点击事件
this.mapIns.on('click', getClickFeature)

getClickFeature(e) {
const vm = this
const { containerPoint } = e
const bbox = this.mapIns.getBounds().toBBoxString()
const size = this.mapIns.getSize()
const url =
`http://gsmap.sf-express.com/geoserver/sfmap/wms` +
`?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetFeatureInfo&` +
`FORMAT=image%2Fpng&TRANSPARENT=true&QUERY_LAYERS=map%3Aexample_layer` +
`&LAYERS=sfmap%3Adept_3&CRS=EPSG%3A4326&` +
`INFO_FORMAT=text%2Fjavascript&FEATURE_COUNT=1000` +
`&X=${containerPoint.x}&Y=${containerPoint.y}` +
`&WIDTH=${size.x}&HEIGHT=${size.y}` +
`&SRS=EPSG%3A4326&STYLES=&BBOX=${bbox}`

function getResult(res) {
if (res) {
// 根据返回的数据结构区域编码
const { features } = res
if (features.length) {
const {
properties: { code },
} = features[0]
vm.$emit('areaClick', code) // 获取到点击位置所属的业务区
}
}
}
this.jsonp(url, getResult) // 通过 jsonp 的方式跨域
}

geoserver 的 GetFeatureInfo 服务支持通过 jsonp 的方式调用,通过组装地址的方式就可以实现请求获取点击位置的所属业务区了,简单看一下请求参数:

  • CRS Coordinate Reference System, 即上文提到的投影标准,这里使用的是 EPSG4326
  • INFO_FORMAT 使用 jsonp 调用所需要的传参
  • FEATURE_COUNT 查询命中的数据对象取数上限
  • X, Y 在地图上的点击坐标,以地图容器左上为原点的坐标值
  • WIDTH, HEIGHT 地图容器的尺寸
  • BBOX Bounds Box, 当前所显示地图的四个角对应的经纬度

geoserver 主要是通过 X, Y, WIDTH, HEIGHT 和 BBOX 来定位到具体的点击位置,需要注意受限于地图缩放等级和容器大小等因素,最终的返回值可能不止一个。

扩展参考:
Leaflet - a JavaScript library for interactive maps
OpenLayers - A high-performance, feature-packed library for all your mapping needs.
SLD Styling — GeoServer 2.19.x User Manual
GeoServer中使用SLD样式
瓦片地图原理