mapbox 基于 Turf.js 实现高精度多边形分割(支持带空洞 / 坐标无损)

在 GIS 前端开发中,多边形分割是高频需求(如图斑拆分、地块划分)。本文基于 Turf.js 封装了一套高精度多边形分割工具类,支持普通模式 / 兼容模式,可处理带空洞的多边形,且能 100% 保留原始坐标,避免 Turf.js 内置方法的坐标偏移问题。

一、功能亮点

  1. 双模式支持
    • 普通模式:要求分割线与多边形外环恰好产生 2 个交点,适用于精准分割场景。
    • 兼容模式:自动过滤分割线,保留与多边形交集部分,支持≥2 个有效交点,容错性更强。
  2. 坐标无损切割:纯手动切割算法,完全保留原始坐标点,无插值偏移,适合对精度要求高的 GIS 系统。
  3. 空洞自动归属:分割后自动将原多边形的空洞分配到对应的子多边形中。
  4. 完善的错误处理:针对参数异常、交点不足等场景抛出明确错误,便于调试。

二、完整代码实现

依赖:确保项目已安装 @turf/turf

html 复制代码
npm install @turf/turf --save

工具类代码:

javascript 复制代码
import * as turf from '@turf/turf';

class ChunkUtil {
  constructor() {
    this.turf = turf;
    this.EPS = 1e-8;
    this.tolerance = 1e-10;
  }

  clipPolygon(polygon, polyline, compatibleMode = false) {
    if (!polygon) throw new Error('未传入目标多边形');
    if (!polyline) throw new Error('未传入分割线');
    if (polygon.geometry?.type !== 'Polygon') throw new Error(`必须是Polygon,当前为${polygon.geometry?.type}`);
    if (polyline.geometry?.type !== 'LineString') throw new Error(`必须是LineString,当前为${polyline.geometry?.type}`);

    let finalLine = polyline;
    if (compatibleMode) {
      finalLine = this._filterLineToPolygon(polyline, polygon);
    }
    return this._setFloors(polygon, finalLine, compatibleMode);
  }

  _setFloors(polygonFeature, polylineFeature, compatibleMode) {
    const polygonBoundary = this.turf.polygonToLine(polygonFeature);
    let boundaryLines = [];
    if (polygonBoundary.geometry.type === 'LineString') {
      boundaryLines = [polygonBoundary];
    } else if (polygonBoundary.geometry.type === 'MultiLineString') {
      boundaryLines = polygonBoundary.geometry.coordinates.map(coords => this.turf.lineString(coords));
    } else {
      throw new Error(`不支持的几何类型:${polygonBoundary.geometry.type}`);
    }

    let targetBoundary = null;
    let validIntersections = [];
    for (const line of boundaryLines) {
      const intersections = this.turf.lineIntersect(line, polylineFeature);
      
      if (compatibleMode) {
        validIntersections = this._filterValidIntersections(intersections.features, polygonFeature);
        if (validIntersections.length >= 2) {
          targetBoundary = line;
          validIntersections = validIntersections.slice(0, 2);
          break;
        }
      } else {
        if (intersections.features.length === 2) {
          targetBoundary = line;
          validIntersections = intersections.features;
          break;
        }
      }
    }

    if (!targetBoundary) {
      const errMsg = compatibleMode 
        ? '分割失败:分割线未与多边形外环产生至少2个有效交点'
        : '分割失败:分割线未与多边形外环产生恰好2个交点';
      throw new Error(errMsg);
    }
    return this._singleClip(targetBoundary, polylineFeature, polygonFeature, validIntersections, compatibleMode);
  }

  _singleClip(polygonBoundary, splitLine, originalPolygon, validIntersections, compatibleMode) {
    const { turf } = this;
    const [p1, p2] = validIntersections;

    const lineCoords = splitLine.geometry.coordinates;
    const startPoint = turf.point(lineCoords[0]);
    const endPoint = turf.point(lineCoords[lineCoords.length - 1]);
    const startIn = turf.booleanPointInPolygon(startPoint, originalPolygon);
    const endIn = turf.booleanPointInPolygon(endPoint, originalPolygon);

    if (compatibleMode) {
      if (startIn && endIn) throw new Error('分割线不能完全在内部');
    } else {
      if (startIn || endIn) throw new Error('分割线起点/终点不能在内部');
    }

    // --- 纯手动切割边界,完全保留阶梯坐标 ---
    const fullRingCoords = polygonBoundary.geometry.coordinates;
    const [clippedBoundaryCoords, remainingCoords] = this._manualSplitRing(fullRingCoords, p1, p2);
    const clippedBoundary = turf.lineString(clippedBoundaryCoords);
    const remainingLine = turf.lineString(remainingCoords);

    // --- 纯手动切割分割线 ---
    const splitLineCoords = splitLine.geometry.coordinates;
    const clippedSplitLineCoords = this._manualSplitLine(splitLineCoords, p1, p2);
    const clippedSplitLine = turf.lineString(clippedSplitLineCoords);

    // 拼接线段
    const mergedLine1 = this._connectLine(clippedBoundary, clippedSplitLine);
    mergedLine1.geometry.coordinates.push(mergedLine1.geometry.coordinates[0]);
    const polygon1Outer = mergedLine1.geometry.coordinates;

    const mergedLine2 = this._connectLine(remainingLine, clippedSplitLine);
    mergedLine2.geometry.coordinates.push(mergedLine2.geometry.coordinates[0]);
    const polygon2Outer = mergedLine2.geometry.coordinates;

    // 空洞归属
    const holes = originalPolygon.geometry.coordinates.slice(1);
    const polygon1Holes = [], polygon2Holes = [];
    const tempPoly1 = turf.polygon([polygon1Outer]);
    const tempPoly2 = turf.polygon([polygon2Outer]);
    for (const hole of holes) {
      const center = turf.centroid(turf.polygon([hole]));
      const in1 = turf.booleanPointInPolygon(center, tempPoly1);
      const in2 = turf.booleanPointInPolygon(center, tempPoly2);
      if (in1 && !in2) polygon1Holes.push(hole);
      else if (in2 && !in1) polygon2Holes.push(hole);
      else if (in1 && in2) {
        const d1 = turf.distance(center, p1);
        const d2 = turf.distance(center, p2);
        d1 < d2 ? polygon1Holes.push(hole) : polygon2Holes.push(hole);
      }
    }

    const polygon1 = turf.polygon([polygon1Outer, ...polygon1Holes], originalPolygon.properties);
    const polygon2 = turf.polygon([polygon2Outer, ...polygon2Holes], originalPolygon.properties);

    [polygon1, polygon2].forEach((poly, idx) => {
      poly.properties = { ...poly.properties, split: true, splitTime: Date.now(), splitId: `${originalPolygon.properties.gid || 'poly'}_split_${idx}` };
    });
    return turf.featureCollection([polygon1, polygon2]);
  }

  // --- 纯手动切割外环,100%保留原始坐标 ---
  _manualSplitRing(ringCoords, p1, p2) {
    const ring = [...ringCoords.slice(0, -1)]; // 移除闭合点
    const insertPoint = (pt) => {
      for (let i = 0; i < ring.length; i++) {
        const a = ring[i], b = ring[i + 1];
        if (this._isPointOnSegment(pt, a, b)) {
          ring.splice(i + 1, 0, pt.geometry.coordinates);
          return;
        }
      }
      ring.push(pt.geometry.coordinates);
    };
    insertPoint(p1);
    insertPoint(p2);

    const idx1 = this._findPointIndex(ring, p1.geometry.coordinates);
    const idx2 = this._findPointIndex(ring, p2.geometry.coordinates);

    let part1, part2;
    if (idx1 < idx2) {
      part1 = ring.slice(idx1, idx2 + 1);
      part2 = ring.slice(idx2).concat(ring.slice(0, idx1 + 1));
    } else {
      part1 = ring.slice(idx2, idx1 + 1);
      part2 = ring.slice(idx1).concat(ring.slice(0, idx2 + 1));
    }
    return [part1, part2];
  }

  // --- 纯手动切割分割线 ---
  _manualSplitLine(lineCoords, p1, p2) {
    const line = [...lineCoords];
    const insertPoint = (pt) => {
      for (let i = 0; i < line.length; i++) {
        const a = line[i], b = line[i + 1];
        if (this._isPointOnSegment(pt, a, b)) {
          line.splice(i + 1, 0, pt.geometry.coordinates);
          return;
        }
      }
      line.push(pt.geometry.coordinates);
    };
    insertPoint(p1);
    insertPoint(p2);

    const idx1 = this._findPointIndex(line, p1.geometry.coordinates);
    const idx2 = this._findPointIndex(line, p2.geometry.coordinates);
    return idx1 < idx2 ? line.slice(idx1, idx2 + 1) : line.slice(idx2, idx1 + 1);
  }

  _findPointIndex(coords, pt) {
    for (let i = 0; i < coords.length; i++) {
      if (this.turf.distance(turf.point(coords[i]), turf.point(pt)) < this.EPS) return i;
    }
    return -1;
  }

  _isPointOnSegment(pt, a, b) {
    const cross = (pt.geometry.coordinates[0] - a[0]) * (b[1] - a[1]) - (pt.geometry.coordinates[1] - a[1]) * (b[0] - a[0]);
    if (Math.abs(cross) > this.EPS) return false;
    const minX = Math.min(a[0], b[0]), maxX = Math.max(a[0], b[0]);
    const minY = Math.min(a[1], b[1]), maxY = Math.max(a[1], b[1]);
    return pt.geometry.coordinates[0] >= minX - this.EPS && pt.geometry.coordinates[0] <= maxX + this.EPS && pt.geometry.coordinates[1] >= minY - this.EPS && pt.geometry.coordinates[1] <= maxY + this.EPS;
  }

  // --- 兼容模式辅助方法 ---
  _filterLineToPolygon(line, polygon) {
    const intersection = this.turf.intersect(line, polygon);
    if (!intersection) throw new Error('分割线与当前图斑无交集');
    if (intersection.geometry.type === 'MultiLineString') {
      const lines = intersection.geometry.coordinates.map(coords => this.turf.lineString(coords));
      lines.sort((a, b) => this.turf.length(b) - this.turf.length(a));
      return lines[0];
    }
    return intersection;
  }

  _filterValidIntersections(points, polygon) {
    const inPolygonPoints = points.filter(point => this.turf.booleanPointInPolygon(point, polygon));
    const uniquePoints = [];
    for (const p of inPolygonPoints) {
      const isDuplicate = uniquePoints.some(u => this.turf.distance(u, p) < this.EPS);
      if (!isDuplicate) uniquePoints.push(p);
    }
    return uniquePoints;
  }

  /**
   * 提取多边形边界中未被切割的剩余坐标
   * @param {Object} fullBoundary - 完整多边形边界线(LineString)
   * @param {Object} clippedBoundary - 切割后的边界线段(LineString)
   * @returns {Array} 剩余坐标数组
   * @private
   */
  _getRemainingBoundaryCoords(fullBoundary, clippedBoundary) { 
     const fullCoords = fullBoundary.geometry.coordinates;
    const clippedCoords = clippedBoundary.geometry.coordinates;
    const isFirstPointMatch = this._isPointInLine(turf.point(fullCoords[0]), clippedBoundary);

    // 1:切割段在边界线头部/尾部
    if (isFirstPointMatch) {
      return fullCoords.filter(coord => !this._isCoordInArray(coord, clippedCoords));
    } 
    // 2:切割段在边界线中间
    else {
      let startPush = false;
      let skipCount = 0;
      const remainingCoords = [];

      for (const coord of fullCoords) {
        if (!this._isCoordInArray(coord, clippedCoords)) {
          if (startPush) {
            remainingCoords.push(coord);
          } else {
            skipCount++;
          }
        } else {
          startPush = true;
        }
      }
      for (let i = 0; i < skipCount; i++) {
        remainingCoords.push(fullCoords[i]);
      }

      return remainingCoords;
    }

  }
  _connectLine(line1, line2) {
    const l1End = line1.geometry.coordinates[line1.geometry.coordinates.length - 1];
    const l2Coords = line2.geometry.coordinates;
    const l2Start = l2Coords[0], l2End = l2Coords[l2Coords.length - 1];
    const merged = [...line1.geometry.coordinates];
    if (this.turf.distance(turf.point(l1End), turf.point(l2Start)) < this.turf.distance(turf.point(l1End), turf.point(l2End))) {
      merged.push(...l2Coords.slice(1));
    } else {
      merged.push(...l2Coords.reverse().slice(1));
    }
    return this.turf.lineString(merged);
  }
  _isPointInLine(point, line) {
    return line.geometry.coordinates.some(coord => this.turf.distance(turf.point(coord), point) < this.EPS);
  }
  _isCoordInArray(coord, coordArray) {
    return coordArray.some(c => this.turf.distance(turf.point(c), turf.point(coord)) < this.EPS);
  }
}

const chunkUtil = new ChunkUtil();
export function splitPolygon(targetPolygon, splitLine, compatibleMode = false) {
  try {
    const splitResult = chunkUtil.clipPolygon(targetPolygon, splitLine, compatibleMode);
    return splitResult?.features || null;
  } catch (error) {
    console.error('多边形分割出错:', error);
    return null;
  }
}
export function createSplitLine(start, end) {
  if (!start || !end || start.length !== 2 || end.length !== 2) return null;
  return turf.lineString([start, end], { name: 'split-line' });
}

三、使用示例

以 Vue3 + mapbox 地图为例,演示如何分割多边形:

javascript 复制代码
<template>
    <div ref="mapContainer" class="map-container" id="map"></div>
</template>
<script setup lang='ts'>
import mapboxgl from 'mapbox-gl'
import { ref, reactive, onMounted, defineProps, watch, onBeforeUnmount, computed, nextTick, inject } from 'vue'
import 'mapbox-gl/dist/mapbox-gl.css'
import MapboxDraw from '@mapbox/mapbox-gl-draw'
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
import baseService from '../../service/baseService'
let map;
const mapContainer = ref()
// ========== 分割相关状态 ==========
const isSplitting = ref(false); // 标记是否处于分割模式
let splitSourceId = "split-temp-source"; // 分割临时数据源ID
let targetSplitFeature = ref(null); // 待分割的目标图斑
const isDrawingSplitLine = ref(false);
// ===== 分割入口方法 =====
const segmentation = () => {
    console.log("进入分割");
    if (!map) {
        ElMessage.warning("地图未初始化完成,无法分割");
        return;
    }
    // 1. 重置分割状态
    resetSplitState();
    // 2. 标记为分割模式
    isSplitting.value = true;
    ElMessage.info("请先点击选中需要分割的图斑");
    // 3. 绑定图斑选中事件
    bindFeatureSelectEvent(handleSplitFeatureClick, "分割");
};
// ===== 重置分割状态 =====
const resetSplitState = () => {
    resetDrawState({
        isModeActive: isSplitting,
        isDrawingLine: isDrawingSplitLine,
        targetFeature: targetSplitFeature,
        drawCompleteHandler: handleSplitDrawComplete,
        drawModeChangeHandler: handleDrawModeChange,
        modeDesc: "分割"
    }, splitSourceId, clearHighlight1);
};

// ===== 处理分割模式下的图斑点击 =====
const handleSplitFeatureClick = (e) => {
    if (isDrawingSplitLine.value) return;
    if (!isSplitting.value || !e.features || e.features.length === 0) return;

    const feature = e.features[0];
    console.log("获取选中的图斑", feature);
    if (!["Polygon", "MultiPolygon"].includes(feature.geometry.type)) {
        ElMessage.warning("仅支持多边形/多多边形图斑分割");
        return;
    }

    targetSplitFeature.value = feature;
    ElMessage.info(`已选中图斑[ID:${feature.properties.gid || '未知'}],请绘制分割线(双击结束)`);
    // 高亮图斑
    highlightFeature1(feature);
    // 激活绘制模式
    isDrawingSplitLine.value = true;
    draw.changeMode("draw_line_string");
    // 监听Draw事件
    map.off('draw.create', handleSplitDrawComplete);
    map.off('draw.modechange', handleDrawModeChange);
    map.on('draw.create', handleSplitDrawComplete);
    map.on('draw.modechange', handleDrawModeChange);
};
// ===== 监听Draw模式变化(分割)=====
const handleDrawModeChange = (e) => {
    if (e.mode === "simple_select") {
        isDrawingSplitLine.value = false;
    }
};
// ===== 处理分割线绘制完成 =====
const handleSplitDrawComplete = (e) => {
    try {
        if (!e || !e.features || e.features.length === 0) {
            ElMessage.error("分割线绘制失败");
            resetSplitState();
            return;
        }
        const drawedLineFeature = e.features[0];
        const splitLineId = e.features[0].id;
        const standardSplitLine = turf.lineString(drawedLineFeature.geometry.coordinates);
        // 调用分割方法
        const splitResult = splitPolygon(targetSplitFeature.value, standardSplitLine);
        console.log("分割数据", splitResult);

        if (!splitResult) {
            ElMessage.error("分割失败:请确保分割线完全贯穿图斑");
            if (draw && splitLineId) {
                draw.delete(splitLineId);
                console.log("分割失败,已清除绘制的分割线");
            }
            resetSplitState();
            return;
        }
        // 提交分割结果
        submitSegmentation(targetSplitFeature.value, splitResult);
        ElMessage.success("分割成功!");
        resetSplitState();
    } catch (error) {
        console.error("分割处理失败:", error);
        ElMessage.error("分割异常,请重试");
        resetSplitState();
    }
};

// ===== 提交分割结果 =====
const submitSegmentation = (oldData, newData) => {
    const data = {
        layerName: oldData.layer.id,
        gid: oldData.properties.gid,
        features: newData
    }
    //提交数据到后端
    baseService.post("/data/boundary/trimming", data, {
        headers: {
            "Content-Type": "application/json"
        }
    }).then(response => {
        // if (response.data.status == '200') {
        //更新分割结果到地图
        // updateSplitPolygonDataSource(oldData.layer.id, newData);
        updateMapSourceWithPlasticResult(oldData.layer.id, newData, oldData.layer.gid)
        updateSegmentationShaping("分割")
        // }
        ElMessage.success({ type: 'success', message: response.data.message })
    }).catch(err => {
        ElMessage.error('失败', err);
    })
};
const updateSegmentationShaping = (s) => {
    if (map && draw) {
        // 2. 用nextTick确保Vue状态更新完成
        nextTick(() => {
            try {
                // 3. 先切回选择模式(强制)
                draw.changeMode('simple_select');
                console.log("当前Draw模式:", draw.getMode()); // 验证是否切成功
                // 4. 清空残留的分割线(同步执行,避免异步残留)
                draw.deleteAll();
                // 5. 同步重置状态(确保响应式更新)
                isDrawingSplitLine.value = false;
                isDrawingTrimLine.value = false;
                if (s == "分割") {
                    isSplitting.value = true;
                    // 6. 验证状态后再绑定事件(避免空绑定)
                    if (isSplitting.value) {
                        console.log("重新分割状态:", isSplitting.value);
                        // 先解绑旧事件,再绑定新事件(避免重复绑定)
                        map.off('click', handleSplitFeatureClick);
                        bindFeatureSelectEvent(handleSplitFeatureClick, "分割");
                    }
                } else if (s == "整形") {
                    isPlasticSurgery.value = true
                    if (isPlasticSurgery.value) {
                        map.off('click', handlePlasticFeatureClick);
                        bindFeatureSelectEvent(handlePlasticFeatureClick, "整形");
                    }

                }
            } catch (err) {

            }
        })

    }
}
/**
 * @param {string} layerName - 原图层名称
 * @param {Array} plasticFeatures - 新图斑数据(Feature数组)
 * @param {string|number} gid - 图斑ID
 * 更新数据
 */
const updateMapSourceWithPlasticResult = async (layerName, plasticFeatures, gid) => {
    try {
        closeDrawLayer(false)
        console.log("更新参数:", { layerName, plasticFeatures, gid });
        let layerIds = map.getStyle().layers
        layerIds.forEach((item) => {
            if (item.id.includes(layerName) && !item.id.includes("_raster")) {
                map.setLayoutProperty(item.id, 'visibility', 'none');
            }
        })
        setTimeout(() => {
            layerIds.forEach((item) => {
                if (item.id.includes(layerName) && !item.id.includes("_raster")) {
                    map.setLayoutProperty(item.id, 'visibility', 'visible');
                }
            })
        }, 500)
    } catch (error) {
        ElMessage.error(`提交失败:${error.message}`);
    }
};
// ===== 高亮分割图斑 =====
const highlightFeature1 = (feature) => {
    // 复用公共高亮方法,返回清除方法
    clearHighlight1 = highlightFeatureCommon(feature, "split-highlight-layer", "#ff9222");
};
/**
 * 公共方法:高亮图斑(分割/整形共用)
 * @param {Object} feature - 要高亮的图斑Feature
 * @param {string} highlightLayerId - 高亮图层ID
 * @param {string} color - 高亮颜色
 */
const highlightFeatureCommon = (feature, highlightLayerId, color) => {
    // 清除旧高亮
    const clearHighlight = () => {
        if (map.getLayer(highlightLayerId)) {
            map.removeLayer(highlightLayerId);
        }
        if (map.getSource(highlightLayerId)) {
            map.removeSource(highlightLayerId);
        }
    };
    clearHighlight();
    // 添加高亮数据源
    map.addSource(highlightLayerId, {
        type: "geojson",
        data: feature
    });
    // 添加高亮图层
    map.addLayer({
        id: highlightLayerId,
        type: "line",
        source: highlightLayerId,
        layout: {
            "line-join": "round",
            "line-cap": "round",
            'line-simplification': 'none',
            'antialias': false
        },
        paint: {
            "line-color": color,
            "line-width": 2,
            "line-opacity": 1
        }
    });
    return clearHighlight; // 返回清除方法
};
/**
 * 公共方法:重置绘制相关状态(分割/整形共用)
 * @param {Object} stateConfig - 状态配置对象
 * @param {string} sourceId - 临时数据源ID
 * @param {Function} clearHighlight - 清除高亮的方法
 */
const resetDrawState = (stateConfig, sourceId, clearHighlight) => {
    // 重置状态变量
    stateConfig.isModeActive.value = false;
    stateConfig.isDrawingLine.value = false;
    stateConfig.targetFeature.value = null;
    // 清理Draw事件监听
    if (map) {
        map.off('draw.create', stateConfig.drawCompleteHandler);
        map.off('draw.modechange', stateConfig.drawModeChangeHandler);
    }
    // 移除临时图层/数据源
    if (map && map.getSource(sourceId)) {
        map.removeSource(sourceId);
    }
    if (map && map.getLayer(`${sourceId}-layer`)) {
        map.removeLayer(`${sourceId}-layer`);
    }
    // 清空Draw绘制要素
    if (draw) {
        draw.deleteAll();
        draw.changeMode("simple_select");
        console.log(`重置${stateConfig.modeDesc}状态,已清空Draw所有绘制要素`);
    }
    // 清除高亮
    clearHighlight();
};
// 全局计数器(分割/整形分开)
let splitCounter = 0;
// 公共排除图层规则(分割/整形共用)
const COMMON_EXCLUDE_LAYER_PATTERNS = [
    /^g-draw-/,       // Draw绘图图层
    /.cold$/,          // Draw冷态图层
    /-hover$/,         // hover高亮图层
    /_road$/,          // 道路图层
    /raster/,          // 栅格底图
    /outline/,         // 轮廓图层
    /city-label/,      // 文字标注图层
    /.hot$/,           // Draw热态图层
    /^guizhou/,        // 贵州边界等非业务图层
    'point',           // 点图层
    'polyline'         // 线图层
];

/**
 * 公共方法:过滤有效业务图层(分割/整形共用)
 * @returns {Array} 过滤后的有效图层ID数组
 */
const getValidBusinessLayerIds = () => {
    if (!map) return [];
    const layerIds = map.getStyle().layers.map(layer => layer.id);
    return layerIds.filter(layerId => {
        return !COMMON_EXCLUDE_LAYER_PATTERNS.some(pattern => {
            if (pattern instanceof RegExp) {
                return pattern.test(layerId);
            } else {
                return layerId === pattern;
            }
        });
    });
};

/**
 * 公共方法:绑定图斑选中事件(分割/整形共用)
 * @param {Function} clickHandler - 点击事件处理函数
 * @param {string} modeDesc - 模式描述(分割/整形,用于日志)
 */
const bindFeatureSelectEvent = (clickHandler, modeDesc) => {
    const validLayerIds = getValidBusinessLayerIds();
    // console.log(`所有图层ID:`, map.getStyle().layers.map(layer => layer.id));
    // console.log(`过滤后可交互的业务图层(${modeDesc}):`, validLayerIds);
    validLayerIds.forEach((layerId) => {
        // 先解绑旧事件,避免重复绑定
        map.off("click", layerId, clickHandler);
        // 绑定新的点击事件
        map.on("click", layerId, clickHandler);
        console.log(`为业务图层绑定${modeDesc}选中事件:${layerId}`);
    });
};
</script>

四、注意事项

  1. 坐标系统 :确保多边形和分割线使用同一坐标系(如 WGS84 或墨卡托),否则会导致交点计算错误。
  2. 交点要求
    • 普通模式下,分割线必须与多边形外环恰好相交 2 次,且起点 / 终点不能在多边形内部。
    • 兼容模式下,分割线可部分在多边形内,但需保证与外环有≥2 个有效交点。

推荐使用 @turf/turf@^6.5.0 版本,低版本可能存在 API 差异。

效果展示:

相关推荐
爱学习的程序媛1 天前
【Web前端】JavaScript设计模式全解析
前端·javascript·设计模式·web
小码哥_常1 天前
从SharedPreferences到DataStore:Android存储进化之路
前端
老黑1 天前
开源工具 AIDA:给 AI 辅助开发加一个数据采集层,让 AI 从错误中自动学习(Glama 3A 认证)
前端·react.js·ai·nodejs·cursor·vibe coding·claude code
薛先生_0991 天前
js学习语法第一天
开发语言·javascript·学习
jessecyj1 天前
Spring boot整合quartz方法
java·前端·spring boot
苦瓜小生1 天前
【前端】|【js手撕】经典高频面试题:手写实现function.call、apply、bind
java·前端·javascript
报错小能手1 天前
深入理解 Linux 虚拟内存管理
开发语言·操作系统
天若有情6731 天前
前端HTML精讲03:页面性能优化+懒加载,搞定首屏加速
前端·性能优化·html
踩着两条虫1 天前
AI驱动的Vue3应用开发平台深入探究(十):物料系统之内置组件库
android·前端·vue.js·人工智能·低代码·系统架构·rxjava
和沐阳学逆向1 天前
我现在怎么用 CC Switch 管中转站,顺手拿 Codex 举个例子
开发语言·javascript·ecmascript