在 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 环境搭建,可直接跳过;若未搭建,执行以下命令:
bash
# 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. 组件完整代码(可直接复用)
javascript
<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 后再渲染。