做 WebGIS 这行的人应该都有体会:面试里真正能把人问住的,往往不是框架 API 怎么调,而是底下的 GIS 基本功。写得再多 OpenLayers 代码、再熟 Leaflet 的插件生态,面试官一句“你这个坐标是 4326 还是 3857,为什么地图上看着对、量出来不对”,或者“十万个点铺上去卡成幻灯片,你怎么优化”,就能把很多人问住。坐标系、投影、空间数据格式、空间索引、瓦片体系、OGC 服务这几块内容,是 WebGIS 岗位面试里反复出现的高频考点。它们决定了你会不会在线上写出一个“看起来能跑、数据全错”的页面。这篇内容适合准备面试的同学,也适合已经干活但基础比较碎片化的从业者:把每个结论背后的原因和参数计算过程写清楚,让面试时能答出来,同时还能清楚地说明为什么这样选择。
1. 坐标系:WebGIS 面试的第一道分水岭
1.1 地理坐标系和投影坐标系,别再用“经纬度”含糊带过
我面过不少人,问“什么是地理坐标系”,回答基本都是“就是经纬度”。这个回答只说了一半,而且容易在后面的追问中暴露问题。地理坐标系(GCS)是球面(严格说是椭球面)上的角度坐标,单位是度,它描述的是点在椭球上的位置,本身没有“距离”这个概念——两个点差 1 度,实际距离在南极和赤道完全不一样。投影坐标系(PCS)是把椭球面按某种数学规则“摊平”到平面上的结果,单位是米,只有到了这一层,你才能用尺子在图上量长度、算面积。
这个区分为什么重要?因为 WebGIS 里绝大多数“量算错误”都源于把这两层混淆了。比如你在 EPSG:4326 的数据上直接调距离接口,算出来一个 0.03 的数,你以为是米,其实是“度”,在地球表面一度大约 111 公里,误差差了五个数量级。反过来,你在 EPSG:3857 的地图上算面积,数字看着挺像样,但在高纬度地区能夸大好几倍。面试时只要把“GCS 是角度、PCS 是米、量算必须在投影平面或球面大地线模型上做”这三句话说清,基本就能过第一关。
再往深一层,投影本身还有分类:等角投影保形状不保面积,等积投影保面积不保形状,等距投影保某一方向距离。Web 地图普遍用的 Web 墨卡托(EPSG:3857)就是等角投影,好处是局部形状不变形、小范围方向上“看着对”,切瓦片也规整;代价是面积失真随纬度急剧放大,格陵兰在图上跟非洲差不多大,实际上非洲面积是它的十几倍。这个取舍是面试官最爱追问的点之一。
1.2 常见 EPSG 速查表和“看数值猜坐标系”的现场办法
现场被问到“你项目里用了哪些坐标系”,最好能张口就来。我整理了一份自己常用的速查表,面试前扫一眼就有底:
| EPSG 代码 | 名称 | 类型 | 单位 | 典型用途 |
|---|---|---|---|---|
| 4326 | WGS84 | 地理坐标系 | 度 | GPS 原始数据、接口传输、GeoJSON 默认 |
| 4490 | 2000 国家大地坐标系 | 地理坐标系 | 度 | 国内测绘成果、政务数据 |
| 3857 | Web 墨卡托 | 投影坐标系 | 米 | Web 底图切片、前端渲染 |
| 32650 | WGS84 / UTM 50N | 投影坐标系 | 米 | 局部区域高精度量算 |
| 4547 | 2000 坐标系 3 度带 114E | 投影坐标系 | 米 | 国内工程测量、规划数据 |
除了背表,还有一个特别实用的现场办法:看数值范围猜坐标系。给你一堆坐标,先看 x、y 的绝对值量级。如果 x 落在 -180 到 180、y 落在 -90 到 90,基本就是 4326 这类地理坐标系;如果数值在 ±20037508.34 附近或者干脆是七八位数的大数,那大概率是 3857。而如果是六位数的 x 加七位数的 y(比如 x 约 50 万、y 约 440 万),这是高斯-克吕格或 UTM 这类投影坐标系的典型特征,前面的六位数还带着带号。这个技巧我在排查线上数据错乱时用过很多次,比翻文档快得多。
EPSG:3857 的那个 ±20037508.34 也不是随便来的,它等于 π 乘以地球赤道半径 6378137 米,也就是把整个赤道周长的一半当作坐标轴的边界。这个数字记住了,面试时顺手推一下,印象分会很高。
1.3 坐标转换怎么做才不丢精度:三参数、七参数与前端工具的真实边界
坐标转换是面试的深水区。基础要求是知道“同一个点在不同坐标系下的坐标值不同,转换需要参数”。再往上一层,要能说清三参数和七参数的区别:三参数只有 X、Y、Z 三个平移量,适用于范围小、精度要求不高的场景;七参数是布尔莎模型,包含 3 个平移、3 个旋转和 1 个尺度因子,公式形如
X' = ΔX + (1 + m) × (X + εz·Y − εy·Z)
其中旋转量通常以角秒为单位,尺度因子 m 以百万分之一(ppm)为单位,这两个单位的换算在实操里最容易出错——我见过有团队把角秒当弧度直接乘进去,结果整片数据偏了几十公里,排查了两天才发现是单位问题。
具体计算时,我通常建议先用最小二乘算参数:拿至少 3 个(七参数建议 6 个以上,且分布均匀)已知在两个坐标系下坐标的控制点,列误差方程求解。这部分的数学面试一般不会让你手推,但会问“参数从哪来、怎么验证”。标准答案应该是:参数来自权威测绘部门或控制点解算,验证靠残差和独立检核点,不能拍脑袋填。
前端做转换,实际项目里绕不开两类工具。一类是通用投影转换库,比如 proj4js,用法很直接:
import proj4 from 'proj4'; // 4326 -> 3857 const [x, y] = proj4('EPSG:4326', 'EPSG:3857', [116.40, 39.90]);另一类是处理国内加密坐标系偏移的工具,像 gcoord、coordtransform 这类小库,负责在标准坐标系和加密坐标系之间互转。这里有个重要的边界认知:这类互转是经验算法,不是严密数学模型,误差在米级,做展示够用,做工程放样绝对不行。面试时如果被问到“你们的坐标转换精度多少”,敢直接说“米级、仅用于展示”比吹“完全精确”要加分得多。
还有一个实际问题,就是精度损失。经纬度用双精度浮点存储完全够用(小数点后 6 位大约对应 0.1 米),但千万别用 float32 存——单精度有效位只有 7 位十进制,116.401234 这种数存进去末尾就废了。我在一个物联网项目里见过后端用 float 存经纬度,导致同一批设备点位在图上会出现随机跳动,排查了很久。
2. 空间数据格式:从 Shapefile 到矢量瓦片的取舍逻辑
2.1 矢量格式三代同堂,选型标准到底是什么
面试里问“你熟悉哪些空间数据格式”,很多人会一口气报出 Shapefile、GeoJSON、KML、GDB、MVT、GPKG。这只能说明你见过,不能说明你会用。真正有价值的回答是说清每个格式的适用边界。
Shapefile 是历史遗留产物,本质是一组文件(.shp 存几何、.shx 存索引、.dbf 存属性、.prj 存坐标系),单文件容量上限 2GB,字段名最长 10 个字符,不支持存时间、不支持拓扑、不支持中文编码统一(.cpg 经常丢)。它的价值只有一个:几乎所有桌面 GIS 软件都认它。所以它适合做数据交换的“中间格式”,不适合做生产存储。
GeoJSON 是 Web 场景的主力,纯文本、结构直观、浏览器原生JSON.parse就能读,和前端生态天然亲近。缺点是体积大——每个坐标都是十进制文本,一个点要写十几个字符,同样一份数据比二进制格式大好几倍;而且解析是同步的,大文件会卡主线程。所以它适合接口传输(几千到几万要素)、适合调试、适合小数据量的静态展示。
矢量瓦片(MVT)是现在的生产级方案。它把矢量数据按金字塔切片,前端按需加载,一份瓦片可以渲染成完全不同的样式,还保留属性可交互。代价是必须有切片流程和客户端渲染能力,适合大数据量、多端复用、需要动态配色的场景。
我的选型口诀是:生产存储用空间数据库、交换协作用 Shapefile、接口传输用 GeoJSON、大规模渲染用矢量瓦片。这四句话在面试里说出来,比罗列十个格式名有用得多。
2.2 GeoJSON 的三个隐藏坑:顺序、环方向、精度
GeoJSON 看着简单,坑不少,而且都是面试官喜欢挖的点。
第一个坑是坐标顺序。GeoJSON 规范规定是[经度, 纬度],也就是[x, y],跟日常口语里“纬度经度”的顺序相反。这个坑极其常见——我用过的某地理编码接口就返回[lat, lng],直接喂给地图就跑到南极去了。排查时我会先看第一个数值有没有超过 90,超了肯定是顺序反了。
第二个坑是环方向(winding order)。按 RFC 7946 的规定,多边形的外环应该是逆时针,内环(洞)应该是顺时针。很多渲染库容错性好,方向不对也能画,但做空间分析、做面积计算、做数据入库时就会出问题——有些库会把方向反的环当成洞,结果是该显示的地方成了窟窿。我的习惯是入库前统一用工具规整一遍环方向,别指望下游都容错。
第三个坑是精度和坐标冗余。原始数据经常带 15 位小数,肉眼根本看不出差别,但体积翻倍。实际项目里保留 6 到 7 位小数(约 0.1 米到 1 厘米)已经完全够用,压缩前做一轮坐标精简(比如 Douglas-Peucker 抽稀)能把体积砍掉一半以上。这里有个经验值可以参考:在 Web 展示级别,抽稀容差取当前缩放层级对应的地面分辨率,肉眼看不出形变。
顺带说一个我踩过的坑:GeoJSON 里出现null几何、或者坐标数组里混了一个null,某些解析库会整个文件报错,另一些会静默跳过。写后端校验时一定要显式检查,别让脏数据流到前端才发现。
2.3 矢量切片到底切了什么:MVT 的编码与体积优化
面试官问“矢量瓦片和 GeoJSON 有什么区别”,能答“一个是二进制一个是文本”只算及格。真正的加分点是说清 MVT 的编码设计。
MVT 底层是 protobuf,几何坐标不是经纬度,而是相对于瓦片本地坐标系的整数,通常瓦片范围是 0 到 4096。用整数有两个好处:一是省空间,二是可以做 zigzag 变长编码(把正负交替映射成无符号数,小数值占更少字节),再叠加 delta 编码(存相邻点的差值而不是绝对坐标),一条密集的线压下来能省一大半。属性部分用字典编码,同一字段的重复字符串只存一次,用索引引用。
体积之外还有传输层的优化。MVT 天然适合 gzip,压缩率通常能到 5 到 10 倍;瓦片按金字塔切分,前端只请求视野内的瓦片,每次请求都是小文件;再配合 CDN 缓存,实际体验会非常流畅。
但这里有个容易被忽略的细节:瓦片缓冲区(buffer)。切片时如果不给瓦片留出边缘余量,一条跨越瓦片边界的线在渲染时会出现断头或者细缝,尤其在放大后特别明显。常见做法是给每个瓦片额外扩出 8 到 64 个单位(对应符号宽度的量级)。这个参数我在项目里调过好几轮,数值太小接缝明显,太大会让瓦片重复数据变多、体积上来。我的经验是线要素取 8 到 16,带符号的点要素按符号像素尺寸除以分辨率再留余量。
另一个高频追问是“为什么矢量瓦片的坐标系固定是 Web 墨卡托”。因为瓦片的金字塔结构、层级编号、分辨率都是按墨卡托网格定义的,换投影整套编号体系就崩了。这一点在面试时能顺口说出来,说明你真的动手切过片。
3. 空间索引:数据一多就卡,问题往往出在这一层
3.1 从暴力遍历到 R 树:几种索引的适用边界
“十万个点渲染卡顿怎么办”是 WebGIS 面试的经典题。很多人第一反应是“用聚合、用热力图、用 canvas”,这些是渲染层的答案,不算错,但没答到根子上。真正决定性能的是空间索引。
最朴素的做法是遍历全部数据做范围判断,复杂度 O(n),一万条以内还行,十万条就是灾难。空间索引的本质是把“二维的无序集合”变成“有组织的层级结构”,让查询从全表扫描变成局部搜索。
R 树是最常被问到的。它用最小包围矩形(MBR)把空间对象一层层包起来,查询时从根节点往下剪枝,只进入与查询框相交的分支。PostGIS 的 GiST 索引、很多空间数据库的默认索引,底层都是 R 树族。面试时值得说出的细节是:普通 R 树节点分裂策略比较粗,容易出现包围盒重叠,重叠越多剪枝效率越低;R* 树优化了分裂算法,尽量降低重叠面积,所以实际性能更好。
四叉树在 Web 场景其实更常见——瓦片金字塔本质上就是一棵四叉树,第 z 层有 4^z 个节点,每次放大一级就往下走一层。所以你在前端做数据分级加载,其实已经在用四叉树思维了。
网格索引最简单:把空间切成固定大小的格子,每个对象登记到它覆盖的格子里。它的优点是实现极简、写入快,缺点是数据分布不均时有的格子爆满、有的空着。做实时打点、物联网位置这种场景,网格索引往往比 R 树更实用,因为写入性能好得多。
还有一种思路是把二维降成一维——Z 序曲线(Morton 码)或者希尔伯特曲线,把经纬度交错编码成一个整数,然后就能直接用普通的 B+ 树、KV 存储来索引。这个思路在时空数据存储里非常常见。
3.2 GeoHash 的二分原理与精度换算
GeoHash 是面试问到“附近的人怎么实现”时的高频答案,但很多人只会说“转成字符串然后前缀匹配”,追一句“为什么前缀相同就代表近”就答不上来了。
原理其实很朴素:把地球当成一张纸,先用 0 度经线切成东西两半,看目标点在东边还是西边,记 1 或 0;再用 0 度纬线切南北,记一位;然后不断二分,每切一次多一位。每 5 位二进制编码成一个字符,用的是 32 个字符的固定字母表(去掉了 a、i、l、o 这几个容易混淆的字母)。所以一个字符代表 5 次二分,字符串越长精度越高。
精度换算我记了一组常用值:
| 位数 | 经度误差 | 纬度误差 | 大约精度 |
|---|---|---|---|
| 4 位 | ±0.0879° | ±0.0440° | 约 20 km |
| 5 位 | ±0.0219° | ±0.0110° | 约 2.4 km |
| 6 位 | ±0.00549° | ±0.00275° | 约 1.2 km × 0.6 km |
| 7 位 | ±0.000686° | ±0.000343° | 约 150 m |
实现起来也不复杂:
BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz" def geohash_encode(lon, lat, precision=7): lat_range, lon_range = [-90.0, 90.0], [-180.0, 180.0] bits, bit, ch, even = [16, 8, 4, 2, 1], 0, 0, True out = [] while len(out) < precision: if even: mid = (lon_range[0] + lon_range[1]) / 2 if lon > mid: ch |= bits[bit] lon_range[0] = mid else: lon_range[1] = mid else: mid = (lat_range[0] + lat_range[1]) / 2 if lat > mid: ch |= bits[bit] lat_range[0] = mid else: lat_range[1] = mid even = not even if bit < 4: bit += 1 else: out.append(BASE32[ch]) bit, ch = 0, 0 return "".join(out)这里有个必须知道的坑:前缀相同只保证在同一个格子里,跨格子的近邻会漏掉。一个点落在格子边界附近,它真正的邻居可能在相邻的 8 个格子里,而编码前缀完全不同。所以“附近查询”的正确做法是:先算出中心点的 GeoHash,取其自身加周围 8 个邻居的编码集合,做前缀匹配,最后再用真实距离过滤一遍。只做前缀匹配不做二次过滤,会出现“查询范围变成阶梯状”的奇怪现象,边界上的点要么被漏掉,要么被拉进来。
GeoHash 还有一个固有缺陷:它是经纬度等分,格子在地球表面不是等面积的,纬度越高格子越“瘦”。做全球尺度的聚合统计时,这种不均匀会带来偏差。所以后来出现了基于球面剖分的方案:S2 把球面投影到立方体六个面再递归四分,优点是层级连续、没有经线跳变问题;H3 用六边形网格,每个单元到中心距离相等,做“半径 k 环”聚合时比正方形的四邻域更规整。面试时提一句“分布均匀性要求高就上球面网格”,是个很稳的加分点。
3.3 PostGIS 索引实操与几个容易写错的查询
面试如果问“你写过空间数据库查询吗”,光说“会写”不够,得能写出正确的语句并解释为什么这么写。
CREATE TABLE poi ( id bigserial PRIMARY KEY, name text, geom geometry(Point, 4326) ); -- 关键一步:建 GiST 索引 CREATE INDEX idx_poi_geom ON poi USING GIST (geom); -- 查询半径 1000 米内的点 SELECT id, name FROM poi WHERE ST_DWithin( geom::geography, ST_SetSRID(ST_MakePoint(116.40, 39.90), 4326)::geography, 1000 );这段代码里有两个细节值得在面试时主动讲。
第一,geometry(Point, 4326)这种带类型和 SRID 的写法,比裸的geometry好,因为它能在插入时做校验,避免混进不同坐标系的数据。我见过最惨的线上事故就是一张表里混了 4326 和 3857 的数据,查询结果一半在地中海、一半在太平洋。
第二,为什么要注意geography和geometry的区别。geometry的距离计算是在平面直角坐标系上做欧氏距离,单位就是坐标系单位;geography则在球面上按大地线算,单位是米。在 4326 上用geometry的ST_DWithin传 1000,它理解成“1000 度”,结果全表返回——这个错误我至少见过三次。而在 3857 上用geometry算距离,赤道附近还算准,到了高纬度会放大,因为墨卡托的尺度因子是 1/cos(纬度),纬度 60 度时误差翻倍,80 度时接近 5.76 倍。
还要注意索引失效的场景。比如写成WHERE ST_Distance(geom, pt) < 1000,函数包裹了字段,GiST 索引用不上,会退化成全表扫描。正确的写法是ST_DWithin,它内部会先用索引做包围盒过滤,再做精确判断。这个区别在数据量上去以后是秒级和分钟级的差异,讲出来很有说服力。
4. OGC 服务与瓦片体系:WMS、WFS、WMTS 到底谁是谁
4.1 服务家族职责划分与选型依据
面试里问“你们地图服务用什么发布的”,标准答案不该只是一个软件名,而应该先分清服务类型。
| 服务 | 全称 | 返回内容 | 能否改样式 | 典型场景 |
|---|---|---|---|---|
| WMS | Web Map Service | 渲染好的图片(PNG/JPEG) | 需要重新请求 | 专题图、栅格底图 |
| WFS | Web Feature Service | 矢量要素(GML/GeoJSON) | 客户端渲染 | 数据下载、要素编辑 |
| WMTS | Web Map Tile Service | 预切好的瓦片 | 固定样式 | 底图、大屏展示 |
| WCS | Web Coverage Service | 栅格原始像元值 | 不涉及 | 高程、气象、遥感反演 |
分辨它们的简单办法是看返回内容:WMS 给图,WFS 给数据,WMTS 给切片,WCS 给数值。WMS 的特点是“服务端渲染”,好处是数据不出服务端、样式受控,坏处是每次改配色都得重新请求,交互性差。WFS 反过来,数据到了客户端,样式随便改,但数据量大时传输压力大。WMTS 是性能最好的——瓦片是预先算好的静态资源,扔到缓存里就行,代价是样式固化,改一次要重新切片。
面试官经常会追问“WFS 和 WFS-T 的区别”。WFS-T 的 T 是 Transaction,支持增删改要素,是编辑类系统的关键能力。能顺口说出这一点,说明你不是只调过只读接口。
还有一个常被问到的实践问题:切片服务的层级怎么定。我的原则是按业务需要的最小层级来切,不要盲目切到 18 级。每多一层,瓦片数量翻四倍,存储和切片时间都是指数增长。全国范围的底图切到 15 级左右通常就够大屏和 Web 展示用,只有在需要看清单个建筑轮廓时才需要更深的层级,而这种需求往往可以用要素查询代替切片。
4.2 瓦片编号与分辨率:把公式自己推一遍
这块内容面试时最容易暴露“只会复制代码”的问题。瓦片编号的推导其实不难,关键是要能现场说清每一步。
设当前层级为 z,那么该层在横竖方向上各有 2^z 个瓦片。经度方向的映射是线性的:
x = (经度 + 180) / 360 × 2^z
纬度方向是非线性的,因为墨卡托投影把纬度按对数拉伸了。推到公式层面,先把纬度转成弧度 φ,墨卡托的 y 坐标是 ln(tan(π/4 + φ/2)),归一化到 0 到 1 之后再乘以 2^z 就是瓦片行号。工程上写成这样更稳:
import math def lonlat_to_tile(lon, lat, z): n = 2 ** z x = int((lon + 180.0) / 360.0 * n) lat_rad = math.radians(lat) y = int((1.0 - math.log(math.tan(lat_rad) + 1.0 / math.cos(lat_rad)) / math.pi) / 2.0 * n) return x, y验证一下边界:纬度取 85.05112878 度时,y 算出来是 0,也就是最上面一行;纬度取 -85.05112878 时得到 n-1。这也正好解释了为什么 Web 地图的南北极永远显示不出来——墨卡托投影在极点是无穷大,为了让整个世界变成一个正方形,只能把纬度裁到 ±85.05112878 度。这个数字可以由“正方形条件”反推:要求投影后的纵向跨度等于赤道周长,解出来正好是这个角度。
分辨率也值得会推。0 级瓦片按 256 像素算,覆盖整个赤道,赤道周长约 40075016.69 米,所以 0 级的地面分辨率是
40075016.69 / 256 ≈ 156543.03 米/像素
每放大一级除以 2:1 级约 78271.5,10 级约 152.87,18 级约 0.597。这个数在项目里特别有用——要判断某个缩放级别能不能看清某类地物,直接查这个表就行。我经常用 15 级(约 4.78 米/像素)作为“能看清街道”的参考线。
还有一个实践细节:TMS 和 XYZ 的 y 轴方向是相反的。XYZ(俗称 Slippy Map 方案)原点在左上角,y 向下递增;TMS 原点在左下角,y 向上递增。转换公式是y_tms = 2^z - 1 - y_xyz。我在对接第三方瓦片源时被这个坑过一次,症状是所有瓦片都在,但南北颠倒了,看起来像整个地球被翻了个面。排查时先看 z=0 那一张,如果它显示的是南极洲,那就是 y 轴方向错了。
4.3 瓦片服务的性能瓶颈与缓存策略
面试中如果问到“你们的瓦片服务怎么保证性能”,回答分三层比较完整。
第一层是预生成。静态底图一律预切,不要用动态渲染顶着。动态渲染(比如 WMS 的 GetMap)每次请求都要读数据、跑样式、栅格化,单张几十到几百毫秒,并发一上来就崩。预切之后瓦片就是静态文件,理论上无限并发。
第二层是缓存分层。浏览器缓存设短一点的过期时间(比如一天),CDN 缓存设长一些,源站只在回源时才真正读文件。这里要注意设置正确的Cache-Control和ETag,以及别忘了给瓦片响应加 gzip——矢量瓦片压缩比很高,图片瓦片就别压了。
第三层是按需切片的兜底。业务数据经常变,不可能全量重切。常见的做法是对变化区域做增量切片,或者用动态切片服务在首次请求时生成并写入缓存,后续走缓存。这里的关键是设置合理的缓存失效策略,否则改了数据、地图上还是老的,用户会以为系统坏了。
还有一个容易被忽略的点:矢量瓦片的坐标精度配置。瓦片内的整数坐标范围(extent)如果设得太大,文件会变大,设得太小,细节会丢。常用的 4096 是权衡结果,相当于每个瓦片内部有 4096 单位的细分精度。这个值在高层级瓦片里够用,在低层级瓦片里反而浪费,但为了统一通常不改。
5. 高频手写题:把“我知道”变成“我写得出来”
5.1 点在多边形内判断:射线法的边界条件
这是 GIS 面试最经典的手写题之一,白板上写过的概率很高。核心思路是射线法:从待测点向右引一条水平射线,统计它与多边形边的交点个数,奇数在内、偶数在外。
function pointInRing(pt, ring) { let inside = false; for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { const [xi, yi] = ring[i]; const [xj, yj] = ring[j]; const hit = (yi > pt[1]) !== (yj > pt[1]) && pt[0] < ((xj - xi) * (pt[1] - yi)) / (yj - yi) + xi; if (hit) inside = !inside; } return inside; }写完之后一定要主动补充边界情况,这是区别人有没有真正写过的地方。
第一,点正好落在边上或者顶点上。上面的实现结果不确定,因为浮点比较是严格的。工业级实现要么单独做一次“点到线段距离是否小于阈值”的判断,要么用固定精度做容差比较。
第二,射线正好穿过顶点。如果一条射线同时穿过两条边的公共端点,会被算成两次相交,奇偶性就反了。上面代码里用(yi > pt[1]) !== (yj > pt[1])这种半开半闭的判断,就是标准解法——把顶点归属固定到一侧,避免重复计数。
第三,多边形带洞。GeoJSON 的 MultiPolygon 有内外环,正确做法是先用外环判断是否在内,再用每个内环判断,只要落在任意一个内环里就属于“不在”。我见过有人直接对所有环做奇偶翻转,结果在多层嵌套的洞里出错。
第四,经纬度直接算的精度问题。在小范围、低纬度场景,用经纬度做平面近似误差可以接受;范围一大就必须投影到平面再做,否则边界判断会歪。
5.2 距离量算与缓冲区:为什么不能直接勾股定理
面试里常有一个陷阱题:“给你两个经纬度,怎么算距离?”很多人张口就是sqrt(dx*dx + dy*dy)乘个系数。这么答基本就结束了。
正确做法至少要说清三种方案及其适用边界。第一种是球面余弦公式,把经纬度转成三维坐标求夹角,再乘地球半径,公式简单但对小距离数值不稳定(两个接近的浮点数相减,精度损失严重)。第二种是哈弗辛公式(Haversine),用半正矢函数规避了小角度下的精度问题,是工程上最常用的方案,误差在千分之几以内。第三种是文森特(Vincenty)公式,基于椭球模型迭代求解,精度能到毫米级,代价是计算慢、个别情况下不收敛。
实操里的经验是:展示用途、城市级范围,Haversine 完全够;测绘、工程放样必须上椭球模型或直接调空间数据库的球面函数。这个分层回答比背公式有说服力得多。
缓冲区的问题同理。在 Web 墨卡托上直接做ST_Buffer(geom, 1000),得到的其实是一个被纬度拉伸过的“椭圆”,在高纬度地区南北方向和东西方向的 1000 米根本不是一回事。正确做法有两个:要么把数据转到局部投影坐标系(比如所在带的 UTM)再缓冲区,要么直接用geography类型让数据库按球面算。我在项目里遇到过“缓冲区分析结果在北纬 50 度偏了将近一倍”的问题,最后就是靠切投影解决的。
顺带说一个相关的坑:面积计算。在地理坐标系上直接算面积,得到的是平方度,毫无意义;在 Web 墨卡托上算,高纬度会严重夸大。要么用球面多边形面积算法,要么用等积投影,要么调数据库的ST_Area(geography)。
5.3 拓扑关系与九交模型的一句话解释
“什么是拓扑关系”这题,能答出“相交、包含、相邻”是及格,能说出九交模型(DE-9IM)是优秀。
九交模型的核心思想很优雅:两个几何对象各自都有内部、边界、外部三个部分,两两组合共 9 种关系,每种关系用“有交集”“无交集”“维度”来描述,形成一个 3×3 矩阵,就能精确刻画两个几何体之间所有可能的拓扑关系。比如“相等”对应T*F**FFF*,“包含”对应T*****FF*,这些字符串就是空间数据库里ST_Relate的返回值。
面试里不必背全表,但要知道两件事:一,二维空间中的拓扑关系是有限的、可以穷举的,这是空间查询能做成通用函数的前提;二,拓扑关系对坐标精度敏感,两个理论上相邻的多边形如果边界点有微小差异,就会判成“不相邻”。所以入库前做拓扑检查(去重复点、捕捉容差、修复自相交)是必要步骤,这一步在很多项目里被跳过了,结果后面的空间关联查询全是漏洞。
6. 排查实录:那些面试官爱追问的“线上问题”
6.1 地图偏移的排查顺序
“地图上数据整体偏移了怎么办”几乎是必问题,因为太常见了。我的排查顺序是固定的,从粗到细一层层排,效率最高:
| 现象 | 最可能的原因 | 验证方式 | 处理方式 |
|---|---|---|---|
| 数据跑到南极或太平洋中间 | 经纬度顺序反了,或 SRID 缺失 | 看数值是否超出 ±90/±180 | 交换顺序,补 SRID |
| 整体偏几百米,形状和底图一致 | 加密坐标系与标准坐标系混用 | 拿已知地标对比偏移方向 | 用对应转换库统一坐标系 |
| 整体偏移且无规律 | 七参数的单位或符号错 | 用控制点回算残差 | 核对角秒/ppm 单位 |
| 缩小看着对,放大就错位 | 层级或分辨率配置不一致 | 检查 z 级与分辨率对照表 | 统一瓦片矩阵定义 |
| 瓦片南北颠倒 | TMS 与 XYZ 的 y 轴方向相反 | 看 0 级瓦片显示的是哪半边 | y 轴翻转 |
| 瓦片之间出现细缝 | 切片 buffer 不足 | 放大边界处观察 | 调大 buffer |
| 只有部分区域错位 | 数据混用了多套坐标系 | 分区统计坐标范围 | 分坐标系分别入库 |
我个人的经验是:先看形状是否变形。如果业务数据的形状和底图上的道路完全对不上、有拉伸或旋转,那是投影问题;如果形状完全一致只是整体平移,那就是坐标系基准或加密偏移问题。这个判断能把排查范围瞬间缩小一半。
6.2 性能问题排查清单
“页面卡”这个描述太笼统,得先定位瓶颈在哪一层。我的习惯是从网络层往上排。
第一步看瓦片请求数量和体积。打开面板数一下单屏请求数,正常在几十个量级,如果上百那就是层级配置有问题或者视野计算错了。再看单个瓦片体积,矢量瓦片单张几百 KB 就算大了,通常是切片时没做抽稀或者属性字段带了一堆无用信息。这个坑我踩过——某次瓦片体积离谱,查了半天发现是切片时把整个属性表都打进去了,删掉几个冗余字段体积直接降了七成。
第二步看要素数量。前端一次渲染几万个点、几万条线,浏览器必然会卡。处理办法按数据量分档:几千个用普通图层就行;上万考虑聚合或者热力图;十万以上必须走矢量瓦片或服务端聚合。这里有个容易被忽略的点:很多图形库默认的命中检测是逐要素遍历的,即使渲染很流畅,鼠标移动时也会卡,需要专门优化。
第三步看样式和交互。阴影、模糊、动画这些效果在移动端极其耗性能;要素级别的onEachFeature绑定事件,要素一多就是上万次绑定。我的做法是改用图层的统一事件加坐标查询,性能能差出十倍。
第四步看内存。长时间运行后变卡,通常是图层没销毁、监听没解绑、瓦片缓存无上限增长。这个在单页应用里特别常见,切换路由时没清理地图实例,来回切几次内存就爆了。
6.3 面试答题的经验之谈
最后说点答题心态上的东西。我带过几个准备面试的同学,发现一个共性问题是:太急着给结论,不给推导过程。面试官问坐标系,你只答“4326 是 WGS84”,这题就浪费了;如果你补一句“它是地理坐标系,单位是度,不能直接拿来算距离,这也是为什么 Web 底图统一用 3857”,这一题就能聊上五分钟,而且聊的全是你熟悉的领域。
第二个经验是主动暴露边界。任何技术方案都有适用范围,敢说“这个方案在我这个数据量下够用,再大就得上别的”比一味强调“我们用了最先进的技术”更可信。我面人的时候,听到“这个我没做过,但我会从哪几个方向去试”,远比听到一个流畅但空洞的背诵要加分。
第三个经验是准备两个自己的案例,越具体越好。不用多,两个就够:一个是你解决过的数据错乱问题,一个是你做过的性能优化。把现象、排查过程、最后怎么定位、改完效果如何,按时间顺序讲清楚。这种叙述在面试里几乎无法被替代,因为它证明你真的动过手。
至于 GIS 这块的知识,说到底就那么几层:坐标系决定数据能不能对得上,数据格式决定传输效率,空间索引决定查询快不快,服务与瓦片决定渲染体验。这四层每一层往下挖都有很多细节,面试能问到的深度其实是有限的,真正的差距在于你有没有在真实数据上被这些问题折磨过。我在实际项目里的体会是,那些踩过的坑最后都变成了面试时最值钱的部分——因为它们没法背,只能经历。