非运维区域汇总功能实现指南

lishihuan大约 7 分钟

非运维区域汇总功能实现指南

📋 功能概述

非运维区域汇总功能是一个地图可视化功能,用于将分散的线段数据合并为连续的区域,并用虚线框在地图上高亮显示

image-20250916101508902
image-20250916101508902

实现效果:将分散的非运维线段合并为统一的虚线框区域,避免相近线路显示为多个分离的区域。

🎯 业务场景

  • 自动合并相近的线段

  • 自动合并相近的区域

  • 在地图上显示合并后的虚线框

🛠️ 技术栈

前端技术

  • 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) {
      // ... 渲染实现
    }
  }
}

📋 总结

实现要点

  1. 数据准备:确保数据库表结构正确,数据格式规范
  2. 算法选择:使用并查集算法进行线段合并
  3. 性能优化:实现空间索引和距离缓存
  4. 错误处理:添加完整的异常处理机制
  5. 测试验证:编写单元测试和集成测试

复用指南

  1. 复制核心算法:线段合并、区域合并、边界框生成

  2. 适配数据格式:根据实际业务调整数据转换逻辑

  3. 调整参数:根据业务需求调整合并阈值

  4. 自定义样式:根据UI设计调整地图样式

  5. 性能调优:根据数据量调整批处理大小