在 WebGIS 开发中,地理数据处理是核心环节 —— 原始地理数据往往存在冗余、格式不统一、范围不符合需求等问题,需要通过裁剪、合并、简化等操作适配业务场景。Turf.js 提供了轻量高效的地理数据处理 API,无需后端依赖即可在浏览器中完成要素裁剪(BBox Clip)、多面合并(Union)、几何简化(Simplify)等核心操作。本文将通过一个地理数据处理组件实战案例,带你掌握 Turf.js 的bboxClip、union、simplify等核心 API,结合 Vue3 + Leaflet 实现可视化数据处理,覆盖从数据加载到结果预览的完整流程。
一、技术栈说明
- 框架:Vue3(Composition API +
<script setup>) - 空间数据处理:Turf.js(@turf/turf v7+,核心 API:
bboxClip、union、simplify、featureCollection) - 地图可视化:Leaflet(轻量级 Web 地图库,支持要素渲染、边界框预览)
- UI 组件库:Element Plus(按钮、输入框、滑块、卡片)
- 样式:Less(模块化样式管理)
- 核心功能:示例地理数据加载、要素裁剪(按边界框)、多面要素合并、几何要素简化、结果可视化与 GeoJSON 预览
二、环境搭建(复用前序环境)
若已完成前序文章的 Vue3 + Turf.js + Leaflet 环境搭建,可直接跳过;若未搭建,执行以下命令:
# 1. 初始化Vue3项目(如需新建) npm create vite@latest turfjs-data-processing -- --template vue cd turfjs-data-processing npm install # 2. 安装核心依赖 npm install @turf/turf element-plus @element-plus/icons-vue leaflet --save npm install less less-loader --save-dev三、核心功能实现:地理数据处理组件
1. 组件完整代码(可直接复用)
<template> <div class="container"> <el-card> <div class="title">地理数据处理组件(裁剪、合并、简化)</div> <div class="main-content"> <!-- 左侧操作面板 --> <div class="left-panel"> <!-- 1. 数据加载区域 --> <div class="section"> <div class="section-title">1. 数据加载</div> <div class="button-group"> <el-button size="small" @click="loadSampleLine">加载示例线</el-button> <el-button size="small" @click="loadSamplePolygon">加载示例面</el-button> <el-button size="small" @click="loadSampleMultiPolygon">加载多面集合</el-button> <el-button size="small" type="danger" @click="clearAll">清空</el-button> </div> </div> <!-- 2. 裁剪操作区域 --> <div class="section"> <div class="section-title">2. 裁剪 (BBox Clip)</div> <div class="input-grid"> <el-input-number v-model="bboxInput[0]" size="small" :step="0.1" placeholder="MinX(最小经度)" /> <el-input-number v-model="bboxInput[1]" size="small" :step="0.1" placeholder="MinY(最小纬度)" /> <el-input-number v-model="bboxInput[2]" size="small" :step="0.1" placeholder="MaxX(最大经度)" /> <el-input-number v-model="bboxInput[3]" size="small" :step="0.1" placeholder="MaxY(最大纬度)" /> </div> <el-button class="action-btn" type="primary" size="small" @click="applyCrop">执行裁剪</el-button> </div> <!-- 3. 合并操作区域 --> <div class="section"> <div class="section-title">3. 合并 (Union)</div> <div class="desc">将集合中的所有面合并为一个面(需至少2个面要素)</div> <el-button class="action-btn" type="primary" size="small" @click="applyUnion" :disabled="!canUnion" >执行合并</el-button> </div> <!-- 4. 简化操作区域 --> <div class="section"> <div class="section-title">4. 简化 (Simplify)</div> <div class="control-row"> <span class="label">精度 (Tolerance): {{ simplifyTolerance }}</span> <el-slider v-model="simplifyTolerance" :min="0.001" :max="0.1" :step="0.001" show-input size="small" /> </div> <el-button class="action-btn" type="primary" size="small" @click="applySimplify">执行简化</el-button> </div> </div> <!-- 右侧地图与结果预览 --> <div class="right-panel"> <!-- 地图可视化区域 --> <div ref="mapEl" class="map"></div> <!-- GeoJSON结果预览 --> <div class="result-area"> <div class="subtitle">处理结果 (GeoJSON)</div> <pre class="code">{{ resultJson }}</pre> </div> </div> </div> </el-card> </div> </template> <script setup> import { ref, onMounted, computed, watch } from 'vue' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import { lineString, polygon, featureCollection, bboxClip, union, simplify } from '@turf/turf' // --- 1. 地图与图层管理 --- const mapEl = ref(null) // 地图容器引用 let map = null // Leaflet地图实例 let inputLayer = null // 原始要素图层(蓝色) let resultLayer = null // 处理结果图层(绿色) let bboxLayer = null // 裁剪框预览图层(红色虚线) // --- 2. 数据状态管理 --- const inputFeatures = ref(null) // 当前输入的Feature/FeatureCollection const resultFeature = ref(null) // 处理后的结果Feature/FeatureCollection const bboxInput = ref([119.0, 29.0, 121.0, 31.0]) // 默认裁剪边界框 [minX, minY, maxX, maxY] const simplifyTolerance = ref(0.01) // 简化精度(默认0.01度,约1公里) // --- 3. 计算属性 --- // GeoJSON结果格式化预览 const resultJson = computed(() => { return resultFeature.value ? JSON.stringify(resultFeature.value, null, 2) : '暂无处理结果' }) // 判断是否可执行合并操作(需至少2个面要素的FeatureCollection) const canUnion = computed(() => { if (!inputFeatures.value) return false if (inputFeatures.value.type === 'FeatureCollection') { // 过滤有效面要素 const validPolys = inputFeatures.value.features.filter(f => f.geometry && ['Polygon', 'MultiPolygon'].includes(f.geometry.type) ) return validPolys.length >= 2 } return false }) // --- 4. 生命周期与监听 --- onMounted(() => { initMap() // 初始化地图 updateMap() // 初始化地图可视化 }) // 监听裁剪框参数变化,实时更新地图上的裁剪框预览 watch(bboxInput, () => { updateBBoxPreview() }, { deep: true }) // --- 5. 地图操作方法 --- // 初始化Leaflet地图 function initMap() { // 创建地图实例:中心坐标(30°N, 120°E),缩放级别6 map = L.map(mapEl.value, { center: [30, 120], zoom: 6 }) // 加载OpenStreetMap底图瓦片 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map) } // 更新裁剪框预览(红色虚线矩形) function updateBBoxPreview() { if (!map) return // 移除旧的裁剪框图层 if (bboxLayer) bboxLayer.remove() const [minX, minY, maxX, maxY] = bboxInput.value // Leaflet矩形边界格式:[[minY, minX], [maxY, maxX]] const bounds = [[minY, minX], [maxY, maxX]] bboxLayer = L.rectangle(bounds, { color: '#ff0000', weight: 1, fill: false, dashArray: '5, 5' }).addTo(map) } // 更新地图可视化(原始要素 + 处理结果) function updateMap() { if (!map) return // 移除旧图层避免重复渲染 if (inputLayer) inputLayer.remove() if (resultLayer) resultLayer.remove() // 创建要素组统一管理图层 const group = L.featureGroup() // 渲染原始要素(蓝色) if (inputFeatures.value) { inputLayer = L.geoJSON(inputFeatures.value, { style: { color: '#3388ff', opacity: 0.6, weight: 2 } }).addTo(group) } // 渲染处理结果(绿色) if (resultFeature.value) { resultLayer = L.geoJSON(resultFeature.value, { style: { color: '#2ecc71', weight: 3 } }).addTo(group) } // 添加要素组到地图并自适应视野 group.addTo(map) updateBBoxPreview() // 更新裁剪框预览 if (group.getLayers().length > 0) { map.fitBounds(group.getBounds(), { padding: [20, 20] }) } } // --- 6. 数据加载方法 --- // 加载示例线要素(浙江省附近折线) function loadSampleLine() { const line = lineString([ [118, 30], [119, 31], [120, 30], [121, 31], [122, 30] ]) setInput(line) } // 加载示例面要素(浙江省附近多边形) function loadSamplePolygon() { const poly = polygon([[ [118, 30], [119, 32], [121, 32], [122, 30], [118, 30] ]]) setInput(poly) } // 加载多面要素集合(两个相邻多边形) function loadSampleMultiPolygon() { const p1 = polygon([[ [119, 30], [119, 31], [120, 31], [120, 30], [119, 30] ]]) const p2 = polygon([[ [120.5, 30.5], [120.5, 31.5], [121.5, 31.5], [121.5, 30.5], [120.5, 30.5] ]]) const fc = featureCollection([p1, p2]) setInput(fc) } // 设置输入数据并重置结果 function setInput(data) { inputFeatures.value = data resultFeature.value = null // 重置处理结果 updateMap() // 更新地图可视化 } // 清空所有数据 function clearAll() { inputFeatures.value = null resultFeature.value = null updateMap() } // --- 7. 核心功能1:要素裁剪(按边界框) --- function applyCrop() { if (!inputFeatures.value) { ElMessage.warning('请先加载地理数据!') return } // 转换裁剪框参数为数字 const bboxArr = bboxInput.value.map(val => Number(val)) // 校验裁剪框参数有效性 if (bboxArr.some(val => isNaN(val)) || bboxArr[0] >= bboxArr[2] || bboxArr[1] >= bboxArr[3]) { ElMessage.error('裁剪框参数无效!请确保 MinX < MaxX 且 MinY < MaxY') return } try { // 处理FeatureCollection:遍历每个要素执行裁剪 if (inputFeatures.value.type === 'FeatureCollection') { const clippedFeatures = inputFeatures.value.features.map(f => bboxClip(f, bboxArr)) resultFeature.value = featureCollection(clippedFeatures) } else { // 处理单个Feature:直接裁剪 resultFeature.value = bboxClip(inputFeatures.value, bboxArr) } updateMap() // 更新地图可视化 } catch (e) { console.error('裁剪失败:', e) ElMessage.error('裁剪失败,请检查输入数据格式!') } } // --- 8. 核心功能2:多面要素合并 --- function applyUnion() { if (!canUnion.value) { ElMessage.warning('请加载至少2个面要素的集合!') return } try { // Turf.union支持直接传入FeatureCollection合并所有面要素 resultFeature.value = union(inputFeatures.value) updateMap() // 更新地图可视化 } catch (e) { console.error('合并失败:', e) ElMessage.error('合并失败,请确保要素为有效面且有重叠/相邻!') } } // --- 9. 核心功能3:几何要素简化 --- function applySimplify() { if (!inputFeatures.value) { ElMessage.warning('请先加载地理数据!') return } // 简化配置项:tolerance(精度)、highQuality(是否高精度简化) const options = { tolerance: simplifyTolerance.value, highQuality: false // 低精度模式性能更高,满足大部分场景 } try { // 深拷贝避免修改原始数据 const inputCopy = JSON.parse(JSON.stringify(inputFeatures.value)) // 执行简化操作 resultFeature.value = simplify(inputCopy, options) updateMap() // 更新地图可视化 } catch (e) { console.error('简化失败:', e) ElMessage.error('简化失败,请检查输入数据格式!') } } </script> <style scoped lang="less"> .container { margin: 24px; text-align: left; } .title { font-size: 18px; font-weight: 600; margin-bottom: 12px; color: #303133; } .main-content { display: flex; gap: 20px; height: 600px; } .left-panel { width: 300px; display: flex; flex-direction: column; gap: 20px; overflow-y: auto; padding-right: 10px; } .right-panel { flex: 1; display: flex; flex-direction: column; gap: 12px; } .section { background: #f8f9fa; padding: 12px; border-radius: 8px; border: 1px solid #ebeef5; } .section-title { font-weight: 600; margin-bottom: 8px; font-size: 14px; color: #303133; } .desc { font-size: 12px; color: #909399; margin-bottom: 8px; line-height: 1.4; } .button-group { display: flex; flex-wrap: wrap; gap: 8px; } .input-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; } .action-btn { width: 100%; } .control-row { margin-bottom: 8px; .label { font-size: 12px; display: block; margin-bottom: 4px; color: #606266; } } .map { flex: 1; border-radius: 8px; overflow: hidden; min-height: 300px; border: 1px solid #ebeef5; } .result-area { height: 150px; display: flex; flex-direction: column; } .subtitle { font-size: 14px; font-weight: 600; margin-bottom: 4px; color: #303133; } .code { flex: 1; overflow: auto; background: rgba(60,60,60,0.8); color: #fff; border-radius: 8px; padding: 8px; font-size: 12px; margin: 0; line-height: 1.4; } </style>2. 核心代码深度解析
(1)Turf.js 核心 API 详解(数据处理重点)
| API | 作用 | 关键参数说明 |
|---|---|---|
bboxClip(feature, bbox) | 按边界框裁剪要素 | -feature:线 / 面 / 多线 / 多面要素;-bbox:边界框数组[minX, minY, maxX, maxY];- 返回裁剪后的要素(超出部分被移除) |
union(featureCollection) | 合并多面要素 | -featureCollection:包含多个面要素的集合;- 返回合并后的单一 / 多面要素;- 仅支持面要素,需至少 2 个有效面 |
simplify(feature, options) | 简化几何要素 | -feature:线 / 面 / 多线 / 多面要素;-options.tolerance:简化精度(单位:度,值越大简化越明显);-options.highQuality:是否高精度简化(默认 false,低精度性能更高) |
featureCollection(features) | 创建要素集合 | -features:要素数组;- 返回标准 GeoJSON FeatureCollection 对象 |
(2)核心逻辑拆解
要素裁剪核心:
- 边界框校验:确保
minX < maxX且minY < maxY,避免无效裁剪; - 批量处理:支持 FeatureCollection(遍历每个要素裁剪)和单个 Feature(直接裁剪);
- 可视化预览:通过红色虚线矩形实时展示裁剪框,裁剪结果以绿色渲染,直观对比原始要素(蓝色)。
- 边界框校验:确保
多面合并核心:
- 前置校验:通过
canUnion计算属性判断是否满足 “至少 2 个有效面要素” 条件; - 容错处理:捕获合并异常(如要素无重叠 / 格式错误),给出友好提示;
- 场景适配:适用于 “行政区划合并”“地理围栏合并” 等需要将多个面整合为一个的场景。
- 前置校验:通过
几何简化核心:
- 精度控制:通过滑块调节
simplifyTolerance(0.001~0.1 度),值越大要素节点越少、形状越简单; - 数据保护:深拷贝原始数据后再简化,避免修改输入数据;
- 性能优化:默认使用
highQuality: false(低精度模式),在保证视觉效果的同时提升处理速度。
- 精度控制:通过滑块调节
可视化优化:
- 图层区分:原始要素(蓝色)、处理结果(绿色)、裁剪框(红色虚线),色彩区分清晰;
- 视野自适应:每次更新数据后自动适配地图视野,确保要素完整显示;
- GeoJSON 预览:格式化展示处理结果的 GeoJSON,便于调试和数据导出。
(3)关键注意事项
数据类型限制:
bboxClip仅支持线 / 面 / 多线 / 多面要素,不支持点要素;union仅支持面要素,线 / 点要素无法合并;simplify支持线 / 面要素,简化点要素无意义(会直接返回原要素)。
简化精度单位:
tolerance单位为 “度”,1 度约等于 111 公里(赤道),因此 0.01 度约等于 1.11 公里,可根据场景调整:- 小范围数据(如城市):使用 0.001~0.01 度;
- 大范围数据(如省份 / 国家):使用 0.01~0.1 度。
性能考量:
- 处理大规模 FeatureCollection 时,建议分批处理,避免页面卡顿;
- 高精度简化(
highQuality: true)适合小要素,大规模数据建议使用低精度模式。
四、功能效果演示
1. 操作流程
数据加载:
- 点击 “加载示例线”/“加载示例面”/“加载多面集合”,地图上显示蓝色原始要素;
- 点击 “清空” 可重置所有数据。
要素裁剪:
- 调整裁剪框参数(MinX/MinY/MaxX/MaxY),地图上实时显示红色虚线裁剪框;
- 点击 “执行裁剪”,地图上显示绿色裁剪结果,下方 GeoJSON 预览区展示裁剪后的要素数据。
多面合并:
- 加载 “多面集合”(至少 2 个面要素);
- 点击 “执行合并”,地图上显示绿色合并结果(两个面整合为一个)。
几何简化:
- 加载任意线 / 面要素;
- 拖动滑块调整简化精度(Tolerance),点击 “执行简化”,地图上显示绿色简化结果(节点减少,形状更简洁)。
2. 示例场景输出
- 要素裁剪:加载示例线(
[[118,30],[119,31],[120,30],[121,31],[122,30]]),裁剪框[119,29,121,31]→ 结果:仅保留[119,31],[120,30],[121,31]段线。 - 多面合并:加载多面集合(两个相邻面)→ 结果:合并为一个包含两个区域的 MultiPolygon。
- 几何简化:加载示例面,简化精度 0.05→ 结果:面要素节点数减少约 50%,形状基本保持不变但数据量更小。
五、代码仓库地址
完整代码已上传至 Gitee,可直接克隆运行:https://gitee.com/tang-yunyan-syp/turfjs-vue3-demo.git
六、专栏地址
本文已同步至 CSDN 专栏,可查看更多 Turf.js 实战内容:https://blog.csdn.net/m0_72065108/article/details/155226062?spm=1001.2014.3001.5501
七、实战拓展方向
- 自定义数据导入:支持上传 GeoJSON 文件加载数据,替代固定示例数据。
- 更多裁剪方式:扩展
clipAPI(按要素裁剪,而非边界框),支持 “用面裁剪线 / 面”。 - 简化结果对比:添加 “节点数统计”,展示简化前后的节点数量,量化简化效果。
- 数据导出:支持下载处理后的 GeoJSON 结果,便于后续使用。
- 批量处理:支持导入多个 GeoJSON 文件,批量执行裁剪 / 合并 / 简化操作。
- 撤销 / 重做:添加操作历史记录,支持撤销上一步处理结果。
八、常见问题排查
裁剪结果为空:
- 原因:要素完全在裁剪框外,或裁剪框参数无效;
- 解决方案:调整裁剪框参数,确保与要素有重叠,或检查参数是否满足
minX < maxX。
合并操作禁用:
- 原因:输入数据不是 FeatureCollection,或有效面要素不足 2 个;
- 解决方案:加载 “多面集合” 示例,或确保上传的 GeoJSON 包含至少 2 个面要素。
简化效果不明显:
- 原因:简化精度(Tolerance)值太小;
- 解决方案:增大 Tolerance 值(如调整到 0.05),或使用高精度简化模式(
highQuality: true)。
地图要素显示偏移:
- 原因:国内底图(高德 / 百度)使用 GCJ-02 坐标系,而示例数据为 WGS84 坐标系;
- 解决方案:集成坐标转换库(如
coordtransform),将 WGS84 坐标转换为 GCJ-02 后再渲染。