非运维区域汇总功能实现指南
大约 7 分钟
非运维区域汇总功能实现指南
📋 功能概述
非运维区域汇总功能是一个地图可视化功能,用于将分散的线段数据合并为连续的区域,并用虚线框在地图上高亮显示

实现效果:将分散的非运维线段合并为统一的虚线框区域,避免相近线路显示为多个分离的区域。
🎯 业务场景
自动合并相近的线段
自动合并相近的区域
在地图上显示合并后的虚线框
🛠️ 技术栈
前端技术
- Vue.js:前端框架
- OpenLayers:地图渲染引擎
- Turf.js:地理空间计算库
- Element UI:UI组件库
后端技术
- Spring Boot:后端框架
- MyBatis:数据持久层
- MySQL:数据库
核心依赖
// 前端依赖
import { Feature, Polygon, VectorLayer, VectorSource } from 'ol'
import { Style, Stroke, Fill } from 'ol/style'
import { fromLonLat, toLonLat } from 'ol/proj'
import * as turf from '@turf/turf'
🚀 完整实现步骤
第一步:后端数据准备
1.1 数据库表设计
-- 线路点表
CREATE TABLE `map_line_point` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`line_id` bigint(20) NOT NULL COMMENT '线路ID',
`line_name` varchar(100) NOT NULL COMMENT '线路名称',
`longitude_gps` decimal(10,6) NOT NULL COMMENT '经度',
`latitude_gps` decimal(10,6) NOT NULL COMMENT '纬度',
`order_num` int(11) NOT NULL COMMENT '排序号',
`eq_tower_id` varchar(50) DEFAULT NULL COMMENT '杆塔ID',
`team_id` bigint(20) NOT NULL COMMENT '团队ID',
`is_yw` tinyint(1) DEFAULT 1 COMMENT '是否运维(1:是,0:否)',
PRIMARY KEY (`id`),
KEY `idx_line_id` (`line_id`),
KEY `idx_team_id` (`team_id`),
KEY `idx_is_yw` (`is_yw`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线路点表';
1.2 后端接口实现
/**
* 非运维区段:返回所有相关线路的相邻点组成的线段集合
* 形如:
* const lineSegments = [
* [[117.8,30.6],[118.0,30.7]],
* [[118.01,30.71],[118.2,30.8]],
* [[118.9,31.2],[119.0,31.3]]
* ]
*/
@Override
public List<List<List<Double>>> selectNotYwLinePoints(Long teamId) {
List<MapLinePointDto> rows = mapBasMapper.selectNotYwLinePoints(teamId);
List<List<List<Double>>> segments = new ArrayList<>();
if (rows == null || rows.isEmpty()) return segments;
Long currentLineId = null;
Double prevLng = null, prevLat = null;
// SQL 已按 line_id, order_num 排序,逐行构造相邻点线段
for (MapLinePointDto row : rows) {
Long lid = row.getLineId();
Double lng = row.getLongitudeGps();
Double lat = row.getLatitudeGps();
// 换线时重置前一点
if (currentLineId == null || !currentLineId.equals(lid)) {
currentLineId = lid;
prevLng = null;
prevLat = null;
}
if (lng == null || lat == null) continue;
if (prevLng != null && prevLat != null) {
List<Double> p1 = new ArrayList<>(2); p1.add(prevLng); p1.add(prevLat);
List<Double> p2 = new ArrayList<>(2); p2.add(lng); p2.add(lat);
List<List<Double>> seg = new ArrayList<>(2); seg.add(p1); seg.add(p2);
segments.add(seg);
}
prevLng = lng;
prevLat = lat;
}
return segments;
}
第二步:前端数据获取
2.1 API接口调用
// 调用后端接口获取非运维区段数据
async initNotYwLinePoints() {
try {
const res = await selectNotYwLinePoints({ teamId: this.teamId })
if (res && res.code === 200 && res.data) {
console.log('获取到非运维线段数据:', res.data.length, '条')
// 直接使用后端返回的线段数据
const boxFeatures = this.buildHighlightBoxes(res.data)
// 创建矢量图层
const vectorLayer = new VectorLayer({
source: new VectorSource({
features: boxFeatures
})
});
// 添加到地图
this.map.addLayer(vectorLayer);
}
} catch (e) {
console.error('[Lines] fetch error:', e)
}
}
第三步:线段合并算法实现
3.1 主处理方法
/**
* 非运维区域汇总主方法
* @param {Array} lineSegments 线段数据 [[[lng1,lat1],[lng2,lat2]], ...]
* @returns {Array} OpenLayers Feature数组
*/
buildHighlightBoxes(lineSegments, projection = 'EPSG:3857') {
const epsilonKm = 0.05 // 线段合并阈值(50米)
const regionMergeKm = 0.1 // 区域合并阈值(100米)
console.log('=== 开始处理非运维区域汇总 ===')
console.log('输入线段数量:', lineSegments.length)
// 第一步:线段级合并
console.log('第一步:开始线段合并...')
const lineGroups = this.mergeLineSegments(lineSegments, epsilonKm)
console.log('线段合并完成,生成组数:', lineGroups.length)
// 第二步:为每个线段组生成边界框
console.log('第二步:开始生成边界框...')
const initialRegions = lineGroups.map(group => this.generateBoundingBox(group))
console.log('边界框生成完成,区域数:', initialRegions.length)
// 第三步:区域级合并
console.log('第三步:开始区域合并...')
const mergedRegions = this.mergeRegions(initialRegions, regionMergeKm)
console.log('区域合并完成,最终区域数:', mergedRegions.length)
// 第四步:转换为OpenLayers Feature
console.log('第四步:开始创建地图要素...')
const features = mergedRegions.map(region => this.createOpenLayersFeature(region))
console.log('地图要素创建完成,数量:', features.length)
console.log('=== 非运维区域汇总处理完成 ===')
return features
}
3.2 线段合并算法
/**
* 线段合并算法 - 并查集实现
* 作用:将相近的线段合并为组
*/
mergeLineSegments(segs, epsilonKm = 0.05) {
const used = new Array(segs.length).fill(false)
const groups = []
// 距离计算函数
const dist = (a, b) => {
try {
return turf.distance(a, b)
} catch (_) {
return Infinity
}
}
// 连接判断函数
const isConnected = (s1, s2) => {
const a0 = s1[0], a1 = s1[1], b0 = s2[0], b1 = s2[1]
return (
dist(a1, b0) <= epsilonKm ||
dist(a1, b1) <= epsilonKm ||
dist(a0, b0) <= epsilonKm ||
dist(a0, b1) <= epsilonKm
)
}
// 并查集合并
for (let i = 0; i < segs.length; i++) {
if (used[i]) continue
used[i] = true
const group = [segs[i]]
let expanded = true
while (expanded) {
expanded = false
for (let j = 0; j < segs.length; j++) {
if (used[j]) continue
if (group.some(g => isConnected(g, segs[j]))) {
used[j] = true
group.push(segs[j])
expanded = true
}
}
}
groups.push(group)
}
return groups
}
3.3 边界框生成算法
/**
* 生成边界框
* 作用:为每个线段组生成最小外接矩形
*/
generateBoundingBox(group) {
const coords = []
group.forEach(seg => {
coords.push(seg[0], seg[1])
})
const line = turf.lineString(coords)
const bbox = turf.bbox(line)
return turf.bboxPolygon(bbox)
}
3.4 区域合并算法
/**
* 区域合并算法
* 作用:将相近的边界框合并为统一区域
*/
mergeRegions(regions, mergeThreshold = 0.1) {
if (regions.length <= 1) return regions
const used = new Array(regions.length).fill(false)
const mergedRegions = []
// 计算区域距离
const getRegionDistance = (region1, region2) => {
try {
const center1 = turf.centroid(region1)
const center2 = turf.centroid(region2)
return turf.distance(center1, center2)
} catch (e) {
return Infinity
}
}
// 合并相近区域
for (let i = 0; i < regions.length; i++) {
if (used[i]) continue
used[i] = true
const currentGroup = [regions[i]]
let expanded = true
while (expanded) {
expanded = false
for (let j = 0; j < regions.length; j++) {
if (used[j]) continue
if (currentGroup.some(groupRegion =>
getRegionDistance(groupRegion, regions[j]) <= mergeThreshold
)) {
used[j] = true
currentGroup.push(regions[j])
expanded = true
}
}
}
// 合并组内区域
const mergedRegion = this.mergeRegionGroup(currentGroup)
mergedRegions.push(mergedRegion)
}
return mergedRegions
}
3.5 区域组合并算法
/**
* 将一组区域合并为一个大的边界框
* 作用:将多个相近区域合并为单个区域
*/
mergeRegionGroup(regions) {
if (regions.length === 1) return regions[0]
try {
// 收集所有区域的坐标点
const allCoords = []
regions.forEach(region => {
const coords = region.geometry.coordinates[0]
allCoords.push(...coords)
})
// 创建包含所有点的线串
const combinedLine = turf.lineString(allCoords)
const bbox = turf.bbox(combinedLine)
return turf.bboxPolygon(bbox)
} catch (e) {
// 如果合并失败,返回第一个区域
return regions[0]
}
}
第四步:地图渲染实现
4.1 OpenLayers Feature创建
/**
* 创建OpenLayers Feature
* 作用:将区域多边形转换为地图可显示的要素
*/
createOpenLayersFeature(region) {
const coords = region.geometry.coordinates[0].map(c => fromLonLat(c))
const feature = new Feature(new Polygon([coords]))
feature.setStyle(new Style({
stroke: new Stroke({
color: '#9e9e9e', // 灰色边框
width: 2, // 边框宽度
lineDash: [8, 6] // 虚线样式
}),
fill: new Fill({
color: 'rgba(158,158,158,0.08)' // 半透明填充
})
}))
return feature
}
4.2 地图图层添加
/**
* 渲染非运维区域到地图
* 作用:将生成的要素添加到地图图层
*/
renderNonYwAreas(features) {
const vectorLayer = new VectorLayer({
source: new VectorSource({
features: features
})
})
this.map.addLayer(vectorLayer)
console.log('非运维区域已渲染到地图,区域数量:', features.length)
}
第五步:完整调用流程
5.1 完整实现代码
// Vue组件中的完整实现
export default {
methods: {
// 主入口方法
async initNotYwLinePoints() {
try {
// 第一步:获取后端数据
const res = await selectNotYwLinePoints({ teamId: this.teamId })
if (res && res.code === 200 && res.data) {
// 第二步:处理线段数据
const features = this.buildHighlightBoxes(res.data)
// 第三步:渲染到地图
this.renderNonYwAreas(features)
}
} catch (error) {
console.error('[Lines] fetch error:', error)
}
},
// 主处理方法(包含所有算法)
buildHighlightBoxes(lineSegments, projection = 'EPSG:3857') {
const epsilonKm = 0.05 // 线段合并阈值
const regionMergeKm = 0.1 // 区域合并阈值
// 第一步:线段级合并
const lineGroups = this.mergeLineSegments(lineSegments, epsilonKm)
// 第二步:生成边界框
const initialRegions = lineGroups.map(group => this.generateBoundingBox(group))
// 第三步:区域级合并
const mergedRegions = this.mergeRegions(initialRegions, regionMergeKm)
// 第四步:创建地图要素
return mergedRegions.map(region => this.createOpenLayersFeature(region))
},
// 线段合并算法
mergeLineSegments(segs, epsilonKm) {
// ... 算法实现
},
// 边界框生成
generateBoundingBox(group) {
// ... 算法实现
},
// 区域合并
mergeRegions(regions, mergeThreshold) {
// ... 算法实现
},
// 区域组合并
mergeRegionGroup(regions) {
// ... 算法实现
},
// 创建地图要素
createOpenLayersFeature(region) {
// ... 算法实现
},
// 渲染到地图
renderNonYwAreas(features) {
// ... 渲染实现
}
}
}
📋 总结
实现要点
- 数据准备:确保数据库表结构正确,数据格式规范
- 算法选择:使用并查集算法进行线段合并
- 性能优化:实现空间索引和距离缓存
- 错误处理:添加完整的异常处理机制
- 测试验证:编写单元测试和集成测试
复用指南
复制核心算法:线段合并、区域合并、边界框生成
适配数据格式:根据实际业务调整数据转换逻辑
调整参数:根据业务需求调整合并阈值
自定义样式:根据UI设计调整地图样式
性能调优:根据数据量调整批处理大小