☰
SuperMap iClient for Leaflet 中 GeoJSON 岛洞数据渲染与环向修复实战
2026/10/9 6:03:19 网站建设 项目流程

干 WebGIS 的兄弟应该都遇到过这种情况:辛辛苦苦整理好的 GeoJSON,在 QGIS 里看一切正常,结果丢到 Leaflet 上一跑,湖中间的岛没了,或者镂空的洞被整片填充成了实心色块。更诡异的是,同一个文件拿 ArcGIS 打开又是好端端的,前端一渲染就开始给你上眼药。我最初接手 SuperMap iClient for Leaflet 项目时,在 GeoJSON 岛洞数据上没少加班,查来查去最后发现,多数问题根本不在图层代码,而在数据环的绕向和填充规则上。

这篇东西就是把我在 SuperMap iClient for Leaflet 里加载、渲染 GeoJSON 岛洞数据的完整实战过程整理出来,包括岛洞数据结构拆解、环境搭建、样式控制、坐标清洗、坑点排查,以及从 SuperMap iServer 数据服务联动前端的内容。适合三类人看:一是刚接触 WebGIS 的新手,想搞明白带洞多边形到底是怎么回事;二是已经在用 Leaflet 或 iClient 做项目的地图开发,正被"洞不透明""岛被吞"之类问题折磨;三是想把手头空间数据库里的岛洞数据发布到 iServer,再接到前端图层的同学。

1. 为什么岛洞数据在 WebGIS 里总出幺蛾子

1.1 岛洞数据结构:一个湖扣一个洞,洞里还有岛

先厘清基础概念。任何一个简单多边形,在 GeoJSON 中的 Polygon 类型里至少得有一个坐标环(ring),这个环叫外环(exterior ring),是图形的外边界。如果这个多边形内部有镂空区域,比如湖泊中间要扣除一块深水区,或者行政区中间有一块飞地不归自己管,GeoJSON 里就要把内环(interior ring)坐标作为 Polygon coordinates 数组里第二个及后续元素写进去。

用人话说,一个带洞的 Polygon 长这样:coordinates 数组的第一个元素是外环,从外环的起点绕一圈回到起点;后面跟着一个或多个内环,每个内环也各自首尾相连,但它们在拓扑上必须完全落在外环内部,不能自相交,也不能彼此嵌套交叉。正常情况下,外环加内环形成的多边形视觉上就是"甜甜圈",被内环圈起来的区域不参与填充。

但如果洞里面还有一块独立地块,比如湖中间有个小岛,这就不是单个 Polygon 能表达的了。GeoJSON 规范里,多个环之间不允许形成"洞中洞"的相互包含关系。想要表达"湖水-深水洞-湖心岛"这种层级,正确的做法是把岛拆成另一个独立的 Polygon 要素。这样整体数据就是一个 FeatureCollection,里面至少有两个 Feature:第一个是带内环的大多边形(湖水区域),第二个是外环独立的岛多边形(湖心岛)。

1.2 GeoJSON 里"岛"和"洞"到底长什么样

我直接给一个最典型的岛洞数据结构。假设有一个近似矩形的湖区,中间有个深水洞,洞里再放一个岛:

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "湖区水面", "type": "water" }, "geometry": { "type": "Polygon", "coordinates": [ [ [100.0, 30.0], [101.0, 30.0], [101.0, 31.0], [100.0, 31.0], [100.0, 30.0] ], [ [100.3, 30.4], [100.6, 30.4], [100.6, 30.6], [100.3, 30.6], [100.3, 30.4] ] ] } }, { "type": "Feature", "properties": { "name": "湖心岛", "type": "island" }, "geometry": { "type": "Polygon", "coordinates": [ [ [100.4, 30.46], [100.52, 30.46], [100.52, 30.55], [100.4, 30.55], [100.4, 30.46] ] ] } } ] }

注意第一个 Polygon 的 coordinates 数组里有两个环,第二个环就是洞的内环。第二个 Feature 的岛多边形,它的外环正好落在前面那个内环的范围里,但它是独立 Polygon。这样前端渲染时,先画带洞的湖区,再画湖心岛,岛的样式就能独立控制。

这也解释了为什么很多新手会把"洞"和"岛"搞混。洞不是一种独立的要素类型,它只是多边形的一个内环;而岛是一个实实在在的多边形要素,只是在视觉上恰好落在另一个多边形的洞里。

1.3 为什么是 SuperMap iClient for Leaflet

市面上能渲染 GeoJSON 的库很多,纯 Leaflet 就能干。但如果是超图生态里的项目,数据源是 SuperMap iServer 发布的 REST 服务,或者需要在超图体系里做前后端一体化的业务,用 SuperMap iClient for Leaflet 是最顺手的路子。它本身就是基于 Leaflet 扩展的 SDK,L.geoJSON 的 API 全都兼容,同时又封装了 iServer 的数据服务请求,能从服务端直接拉取要素和属性,不用自己拼 REST 请求。选择它做基础环境,不是为了炫技,而是为了贴近真实项目里"数据在 iServer、展示在前端"的典型链路。

2. 环境搭建与加载 GeoJSON 的基础姿势

2.1 引入 iClient for Leaflet 依赖

SuperMap iClient for Leaflet 的引入方式和原生 Leaflet 相似,页面里先引 CSS,再引 JS 和超图扩展库。官网提供了 CDN 地址,也可以通过 npm 包安装后在工程里引用。这里以 CDN 方式作为示例,版本号请以实际发布版本为准:

<link rel="stylesheet" href="https://iclient.supermap.io/cdn/leaflet/1.3.4/leaflet.css" /> <script src="https://iclient.supermap.io/cdn/leaflet/1.3.4/leaflet.js"></script> <script src="https://iclient.supermap.io/cdn/leaflet/1.3.4/leaflet.supermap.js"></script>

如果你的项目用了 Vue 或者 React,我更推荐用 npm 方式安装@supermap/iclient-leaflet,然后按模块 import,方便打包和版本管理。不过无论哪种引入方式,地图初始化和 GeoJSON 图层加载的核心逻辑是相通的。

2.2 先跑通本地 GeoJSON

先把最简单的本地数据跑通,再谈岛洞。创建地图容器后,初始化一个地图实例,然后通过L.geoJSON把 FeatureCollection 丢进去:

var map = L.map('map', { center: [30.5, 100.5], zoom: 9 }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'OSM' }).addTo(map); var geojsonLayer = L.geoJSON(featureCollection, { style: function (feature) { return { color: '#333333', weight: 1, fillColor: '#3ba2e0', fillOpacity: 0.7 }; } }); geojsonLayer.addTo(map); map.fitBounds(geojsonLayer.getBounds());

这一套跑通后,你会在地图上看到一片蓝色区域。但注意,如果此时你用的是前面那个"湖区 + 深水洞 + 湖心岛"的数据,大概率会出现两种情况:要么整个矩形全被蓝色填充,深水洞完全看不见;要么洞是透明的,但湖心岛却也被透明掉了。后面第 3 节会重点讲原因。

2.3 从 iServer REST 数据服务拉取 Feature

本地文件能跑通后,要把数据换成 iServer 服务里的要素。iClient 封装的 featureService 可以省去手写 REST 请求的麻烦。典型流程是先请求服务,再把返回的要素喂给L.geoJSON:

var serviceUrl = 'http://localhost:8090/iserver/services/data-world/rest/data'; L.supermap.featureService(serviceUrl) .getFeaturesBySQL({ attributeFilter: "SMID < 1000" }) .then(function (result) { var layer = L.geoJSON(result.features, { style: function (feature) { return { color: '#666', weight: 1, fillColor: '#f5c242', fillOpacity: 0.6 }; } }); layer.addTo(map); });

这里有个小细节:iServer 查询结果里的features字段通常就是符合 GeoJSON 规范的 Feature 集合,可以直接交给L.geoJSON。但实际接口版本不一样,字段名可能会有差异,建议在then里先console.log(result)看一眼再继续,避免拿到一个莫名的对象导致前端渲染失败。

3. 岛洞渲染的关键:填充规则与坐标绕向

3.1 兵家常事:外环内环绕向不一致

岛洞渲染最容易踩的坑,就是环的绕向问题。在 SVG 和 Canvas 的填充算法里,多边形的内外关系很大程度上取决于各环的"方向"。SVG path 的默认填充规则是 nonzero(非零环绕规则),简单理解就是:从图形内任一点朝外发射一条射线,统计射线与所有环的交点,同向的路径相交计 +1,反向计 -1,如果最终结果不为 0,这个点就在填充区域内。

当外环是逆时针、内环是顺时针时,洞内部的 winding 计数正好抵消为 0,于是不填充,视觉上形成镂空。但如果你手里的数据内外环都是逆时针,或者都是顺时针,nonzero 规则下洞区域的计数并不会抵消,结果就是整个外环内部全部被填充,洞直接"消失"。

很多从 ArcGIS 导出的 GeoJSON 都会自动处理环向,但手工编辑、爬虫抓取、不同平台之间导来导去的数据,绕向经常是乱的。所以加载岛洞数据之前,强烈建议先写一个清洗函数,把所有 Polygon 强制规范成"外环逆时针、内环顺时针"。这个约定既符合 RFC 7946 的推荐,也能兼容绝大多数渲染引擎。

3.2 用鞋带公式批量清洗环向

坐标环的方向可以通过鞋带公式(Shoelace Formula)计算有向面积来判断。面积为正通常对应逆时针方向,面积为负对应顺时针方向。注意经纬度坐标下直接算出来的值不一定直接代表我们习惯的"上下左右",但对于判断相对方向已经足够。清洗函数如下:

function ringArea(ring) { var area = 0; var len = ring.length; for (var i = 0, j = len - 1; i < len; j = i++) { area += (ring[j][0] * ring[i][1]) - (ring[i][0] * ring[j][1]); } return area / 2; } function reverseRing(ring) { return ring.slice().reverse(); } function rewindGeometry(geometry) { if (!geometry || !geometry.type) return geometry; if (geometry.type === 'Polygon') { geometry.coordinates = geometry.coordinates.map(function (ring, index) { if (index === 0) { return ringArea(ring) >= 0 ? ring : reverseRing(ring); } return ringArea(ring) <= 0 ? ring : reverseRing(ring); }); } else if (geometry.type === 'MultiPolygon') { geometry.coordinates = geometry.coordinates.map(function (polygon) { return polygon.map(function (ring, index) { if (index === 0) { return ringArea(ring) >= 0 ? ring : reverseRing(ring); } return ringArea(ring) <= 0 ? ring : reverseRing(ring); }); }); } return geometry; }

在把数据交给L.geoJSON之前,对每个 Feature 的 geometry 执行一下rewindGeometry,大部分"洞被填充"的问题就解决了。我实际项目里只用这一步就处理掉了百分之八九十的岛洞异常。如果你不想手写,可以用 Turf.js 的turf.rewind,一行命令完成同样的事情。

3.3 fill-rule 到底听谁的

清洗数据是治本方案,但有些时候数据量大、不想做全量预处理,想靠前端渲染器兜底。这时就要理解 fill-rule 的作用。Leaflet 默认渲染器是 SVG,Canvas 渲染器需要手动指定preferCanvas: true。对于带洞多边形,理论上无论 SVG 还是 Canvas,只要数据环向正确,都能正常镂空。

但问题是,Leaflet 的 Polygon 并没有在style里直接提供一个稳定可靠的fillRule选项用于所有版本。我踩过的坑是:给 path 传了fillRule: 'evenodd',结果渲染出来的 path 元素上根本没写这个属性,洞照样被填上。因为 Leaflet 的渲染器只是按数据坐标生成 path 的 d 字符串,默认的填充规则不归它管。

所以我的经验是:不要依赖前端设置 fill-rule,老老实实在数据层把环向规整好。如果你实在不想清洗数据,有一个 hack 办法是在 SVG 渲染模式下,等图层 add 完后手动遍历 path 元素,加上fill-rule="evenodd"属性。但这个方法在 Canvas 渲染器下无效,而且遍历 DOM 本身就不是正道,只能作为临时调试手段。

3.4 style 回调:按要素类型区分湖面、洞与岛

环向问题解决后,样式控制就变得很直观。岛洞数据在视觉上最需要区分的是:外多边形、洞区域、岛地块。由于洞其实是内环造成的透明区域,它本身没有独立的 Feature 对象,所以"洞"的样式取决于外多边形多边形的填充色和底图的关系。而岛是独立 Feature,可以在style回调里单独识别。

实际代码里我一般这样处理:

var layer = L.geoJSON(rewindFeatureCollection(featureCollection), { style: function (feature) { if (feature.properties.type === 'island') { return { color: '#8B5A2B', weight: 2, fillColor: '#35b066', fillOpacity: 1 }; } if (feature.properties.type === 'water') { return { color: '#1a7fc1', weight: 1, fillColor: '#3ba2e0', fillOpacity: 0.7 }; } return { color: '#888', weight: 1, fillColor: '#eee', fillOpacity: 0.5 }; } });

注意,如果岛 Feature 和外多边形 Feature 处在同一个L.geoJSON图层里,图层内部的绘制顺序是按 Feature 顺序来的。带洞的外多边形必须排在岛前面。如果顺序反过来,后画的湖面半透明填充会把岛压住,视觉上岛就被"水淹没"了。

4. 实战:一个带岛带洞的"鱼塘"示例

4.1 设计场景与数据

为了把这个过程完整走一遍,我设计一个实际场景:一片鱼塘养殖区,外边界是一个大的不规则四边形;鱼塘中间有一小块深水区,深水区不画水,作为洞显示底图;深水区中央还有一个泥质小岛,上面长了一丛观景植物,作为独立要素渲染。这样的数据既能体现"洞",又能体现"岛",还能直观检验渲染层级。

数据我直接用上一节的 FeatureCollection,只是把坐标改得更贴近真实业务一点,同时把第二个 Feature 的岛放在洞的范围内。为了让你看得更明白,我把两个 Feature 分开命名:鱼塘水面和湖心岛。

4.2 完整示例代码

完整的 HTML 文件不需要分成模块,直接在单页里就能跑:

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>SuperMap iClient for Leaflet 岛洞数据渲染示例</title> <link rel="stylesheet" href="https://iclient.supermap.io/cdn/leaflet/1.3.4/leaflet.css" /> <script src="https://iclient.supermap.io/cdn/leaflet/1.3.4/leaflet.js"></script> <script src="https://iclient.supermap.io/cdn/leaflet/1.3.4/leaflet.supermap.js"></script> <style> html, body, #map { height: 100%; margin: 0; } </style> </head> <body> <div id="map"></div> <script> var map = L.map('map', { center: [30.5, 100.5], zoom: 11 }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'OSM' }).addTo(map); var featureCollection = { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "鱼塘水面", "type": "water" }, "geometry": { "type": "Polygon", "coordinates": [ [[100.0, 30.0], [101.0, 30.0], [101.0, 31.0], [100.0, 31.0], [100.0, 30.0]], [[100.3, 30.4], [100.6, 30.4], [100.6, 30.6], [100.3, 30.6], [100.3, 30.4]] ] } }, { "type": "Feature", "properties": { "name": "湖心岛", "type": "island" }, "geometry": { "type": "Polygon", "coordinates": [ [[100.4, 30.46], [100.52, 30.46], [100.52, 30.55], [100.4, 30.55], [100.4, 30.46]] ] } } ] }; function ringArea(ring) { var area = 0; var len = ring.length; for (var i = 0, j = len - 1; i < len; j = i++) { area += (ring[j][0] * ring[i][1]) - (ring[i][0] * ring[j][1]); } return area / 2; } function reverseRing(ring) { return ring.slice().reverse(); } function rewindGeometry(geometry) { if (!geometry || !geometry.type) return geometry; if (geometry.type === 'Polygon') { geometry.coordinates = geometry.coordinates.map(function (ring, index) { if (index === 0) return ringArea(ring) >= 0 ? ring : reverseRing(ring); return ringArea(ring) <= 0 ? ring : reverseRing(ring); }); } else if (geometry.type === 'MultiPolygon') { geometry.coordinates = geometry.coordinates.map(function (polygon) { return polygon.map(function (ring, index) { if (index === 0) return ringArea(ring) >= 0 ? ring : reverseRing(ring); return ringArea(ring) <= 0 ? ring : reverseRing(ring); }); }); } return geometry; } featureCollection.features.forEach(function (feature) { feature.geometry = rewindGeometry(feature.geometry); }); var islandLayer = L.geoJSON(featureCollection, { style: function (feature) { if (feature.properties.type === 'island') { return { color: '#8B5A2B', weight: 2, fillColor: '#35b066', fillOpacity: 1 }; } return { color: '#1a7fc1', weight: 1, fillColor: '#3ba2e0', fillOpacity: 0.75 }; }, onEachFeature: function (feature, layer) { layer.bindPopup(feature.properties.name); } }); islandLayer.addTo(map); map.fitBounds(islandLayer.getBounds()); </script> </body> </html>

4.3 预期效果与验证方法

跑起来之后,你应该看到:蓝色鱼塘水面覆盖外环范围内的大部分区域;中部一块透明区域就是深水洞,它把底图透出来;深水洞中央有一块绿色岛。点击蓝色水面,弹出"鱼塘水面";点击绿色岛,弹出"湖心岛"。

有一个很容易忽略的验证点:岛不能和洞"粘连",必须保留一点间距。如果岛多边形边界碰触到内环边界,某些渲染引擎会因为拓扑错误出现细缝或整块填充异常。数据准备阶段最好避免这种边界相接的情况,实在无法避免,就做一次 buffer 处理把岛缩小一点点。

5. 踩坑实录:常见问题与排查速查表

5.1 洞被填充了怎么办

这是出现频率最高的问题。排查顺序我建议按下面的来:

  1. 先确认内环坐标是不是真的放在同一个 Polygon 的 coordinates 数组里。有些时候操作人员会把内环单独当成一个 Feature,这样渲染出来不是一个洞,而是两个独立多边形叠加。
  2. 再确认内外环绕向。写一个清洗函数统一为外环逆时针、内环顺时针。
  3. 最后看图层顺序。带洞的外多边形应该排在内部要素前面,否则后画的半透明填充把它盖住,视觉上就看不到洞。

如果三者都没问题,试着在浏览器开发者工具里选中 SVG/Canvas 生成的图形,看当前 path 的填充规则是不是被样式表覆盖了。

5.2 ArcGIS 打开正常,Leaflet 渲染不对

有不少人问过"GeoJSON 用 ArcGIS 打开正常,为什么放到 SuperMap iClient for Leaflet 里就渲染不对"。ArcGIS 对 GeoJSON 的容错能力比较强,很多工具在导入时自动修复环向和几何问题,所以你在 ArcGIS 里看到的是"被纠正后"的数据。但前端渲染引擎拿到的还是原始 GeoJSON,没有做自动修复。

应对办法很直接:用 ArcGIS 打开后做一个检查,或者直接导出清洗后的数据。如果项目里必需用 ArcGIS 协同,可以在导出时选择字段、坐标系,不要勾选"简化要素"之类的选项,尽量减少 ArcGIS 在前端不可见的隐式处理。

5.3 岛洞区域的点击事件不命中

带洞多边形的交互边界和填充区域一致,也就是说,洞区域不属于该多边形。点击洞区域时,如果洞底下没有别的要素,Leaflet 会判定没有命中也正常。但如果你希望点击洞区域时触发某个动作,需要额外在洞范围上再叠一层不可见的透明图层。我这里提供一个思路:把内环反向提取成一个独立 Polygon,绑定对应的事件,设置成interactive: false的兄弟图层,专门负责交互。别看这个需求小,我踩过坑,直接在带洞多边形上监听,洞区域怎么点都没反应,排查了好久才明白是命中规则在起作用。

5.4 数据量大渲染卡顿

岛洞数据如果来自高精度测绘,一个 Polygon 可能会有几万个顶点。L.geoJSON在加载大量要素时,DOM 数量会暴增,地图拖动会明显掉帧。我的建议是:优先考虑在服务端或者 iDesktop 里做数据抽稀,而不是把超高精度数据直接交给浏览器。如果前端还必须保留较高精度,用preferCanvas: true开启 Canvas 渲染器,对大量矢量要素的绘制性能会好很多。

var map = L.map('map', { preferCanvas: true });

不过 Canvas 渲染器对单个 path 的样式细节支持不如 SVG 细腻,比如边线有时会有锯齿,自己权衡。

5.5 地图旋转与命中检测偏移

有些项目会用 Leaflet 地图旋转插件,比如给地图加一个旋转角。这时候需要注意,旋转后的图层虽然视觉上转动了,但点击事件的命中检测在某些版本里会存在偏差,尤其是带岛洞的复杂多边形,容易出现"点不到"或"点到另一个要素"的情况。我的建议是:在应用地图旋转前先确认 Leaflet 和 iClient 版本兼容,遇到岛洞要素的交互需求时,尽量采用"旋转容器而不旋转数据"或者关闭旋转交互的方案。这个点在普通项目里可能不常见,但如果用到了,一定要提早在原型阶段做测试,别等渲染阶段才发现所有多边形点位全偏了。

5.6 GeoJSON 能不能用 ArcGIS 打开

顺带回答一个同行经常问的问题。GeoJSON 本身是纯文本格式,ArcGIS Pro 和 ArcMap 都支持通过工具转为要素类,或者直接拖入某些版本的 ArcGIS Pro 中预览。ArcGIS 在打开时通常能自动识别坐标和几何类型,但对超大 GeoJSON 文件建议先拆分再导入。这里和前端渲染的关系在于:同一份数据,在 ArcGIS 里正常不代表在 Leaflet 里就正常,因为前端不会做几何修复,所以数据清洗这一步不能省。

5.7 踩坑速查表

现象常见原因处理办法
洞被全部填充内环外环同向绕向用鞋带公式统一为外环逆时针、内环顺时针
洞区域被半透明色遮挡Feature 顺序颠倒带洞外多边形优先添加
岛样式被水面样式盖掉图层渲染顺序或样式判定优先级在 style 回调里按 type 属性区分
点击洞区域没反应洞不属于外多边形命中区域单独叠一个透明交互图层
地图移动卡顿顶点过多、SVG DOM 过多数据抽稀 + preferCanvas
ArcGIS 正常 Leaflet 异常ArcGIS 自动修复了几何以清洗后的原始数据为准
岛与洞连接处出现缝边界相接拓扑错误数据准备时保留间距,或对岛做轻微 buffer

6. 从 iServer 发布到前端联动的延伸实践

6.1 用 iDesktop 将数据入库并发布

如果数据还在本地文件里,想上到 SuperMap iServer 服务,一般先在 SuperMap iDesktop 里导入 GeoJSON 或 Shapefile 数据,入库形成矢量数据集,再在 iServer 里注册数据源并发布。发布后能得到一个 REST 数据服务地址,前端就可以通过L.supermap.featureService来请求其中要素。

这步通常不复杂,但有一个容易踩的点:iDesktop 在入库时如果源数据坐标参考系和目标数据源不一致,可能会导致要素偏移。岛洞数据尤其要小心,因为洞和岛往往来自不同编辑批次,一旦坐标系错位,很可能洞与岛相对位置完全对不上。入库后建议在 iDesktop 里放大检查一遍,确认内外环拓扑关系没有被破坏。

6.2 KingbaseES V8 空间库的数据发布流程

项目里有人在问 KingbaseES V8 如何在 SuperMap iServer 上发布数据。KingbaseES 是国产关系型数据库,空间数据落地方式通常是在数据库里建空间字段,存放矢量几何信息。SuperMap iServer 要接 KingbaseES,一般流程是:先确保 iServer 所在机器有对应的 KingbaseES JDBC 驱动,并在 iServer 里注册数据源连接;然后在 iDesktop 或管理页面中创建数据集,利用 SQL 查询把空间几何读取出来;最后发布成 REST 数据服务。

有一个关键点:KingbaseES 里空间类型定义、SRID、几何字段名都需要和 iServer 文档保持一致。如果几何字段类型不对,发布时会出现数据源识别不了或要素全丢失。发布完成后,最好先用 iServer 的查询功能试查一条数据,确认返回的要素坐标是否正常,再去前端对接。

6.3 前端直接用 featureService 加载岛洞要素

数据服务发布好以后,前端代码可以写成这样:

var serviceUrl = 'http://localhost:8090/iserver/services/data-world/rest/data'; function loadIslandData() { L.supermap.featureService(serviceUrl) .getFeaturesBySQL({ attributeFilter: 'SMID > 0' }) .then(function (result) { var features = result.features; if (!features || !features.length) return; features.forEach(function (feature) { feature.geometry = rewindGeometry(feature.geometry); }); var layer = L.geoJSON(features, { style: function (feature) { if (feature.properties.type === 'island') { return { color: '#8B5A2B', weight: 2, fillColor: '#35b066', fillOpacity: 1 }; } return { color: '#1a7fc1', weight: 1, fillColor: '#3ba2e0', fillOpacity: 0.75 }; } }); layer.addTo(map); map.fitBounds(layer.getBounds()); }) .catch(function (err) { console.log('请求数据失败', err); }); }

这里要再次强调数据清洗的时机。从数据库和 iServer 出来的几何,绕向不一定已经规范化。我在某个项目里没有在请求后清洗环向,结果服务器返回的岛洞数据外环全是顺时针,前端一渲染,所有水深区全部被填掉,被业务方当场抓包。后来我干脆把清洗函数做成了公共工具,所有进入前端的 GeoJSON 一律先过一遍rewindGeometry,再也不在渲染层跟数据较劲。

一点个人心得

最后分享一个我自己踩过不少次的小教训。进入 SuperMap iClient for Leaflet 的岛洞项目之后,我最大的体会是:前端渲染问题十有八九不是渲染器的问题,而是数据本身的问题。遇到洞被填充、岛被盖住、点击不命中这类现象,先冷静下来检查数据——内外环顺序对不对,绕向统不统一,Feature 顺序合不合理,坐标系有没有偏。把数据梳理干净了,前端代码往往只是一段很简单的L.geoJSON加上一个style回调而已。

数据清洗这段代码不复杂,但真的值得沉淀成公共函数。我在多个项目里都用同一套rewindGeometry,也把它推荐给了不少同事。岛洞数据看着麻烦,本质上就是理解"洞是内环、岛是独立要素"这一层关系,再叠加一层对环向的敬畏,整个链路就没那么玄乎了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询