医疗机构网站模板,网站开发实现总结,网站文字格式,天津百度推广公司在 WebGIS 开发中#xff0c;空间关系判断是高频核心需求 —— 例如外卖配送中 “判断用户是否在配送范围内”、“规划路线是否穿过禁行区域”、“查找离用户最近的骑手” 等场景#xff0c;都依赖于空间关系分析能力。Turf.js 提供了一套完整的空间关系判断 API#xff0c;…在 WebGIS 开发中空间关系判断是高频核心需求 —— 例如外卖配送中 “判断用户是否在配送范围内”、“规划路线是否穿过禁行区域”、“查找离用户最近的骑手” 等场景都依赖于空间关系分析能力。Turf.js 提供了一套完整的空间关系判断 API支持点在面内、线面相交、最近邻查询等核心操作。本文将通过一个配送场景空间关系校验工具实战案例带你掌握 Turf.js 的booleanPointInPolygon、booleanCrosses、nearestPoint等核心 API结合 Vue3 Leaflet 实现可视化空间分析覆盖从基础包含判断到高级邻近查询的完整流程。一、技术栈说明框架Vue3Composition API script setup空间分析Turf.jsturf/turf v7核心 APIbooleanPointInPolygon、booleanCrosses、nearestPoint、distance地图可视化Leaflet轻量级 Web 地图库支持要素绘制、交互点击选点UI 组件库Element Plus折叠面板、标签页、表单组件、按钮样式原生 CSS模块化样式管理核心功能配送区域面编辑、点在面内判断、路径与区域相交检测、最近邻要素查询骑手匹配、地图交互选点二、环境搭建复用前序环境若已完成前序文章的 Vue3 Turf.js Leaflet 环境搭建可直接跳过若未搭建执行以下命令# 1. 初始化Vue3项目如需新建 npm create vitelatest turfjs-spatial-relation -- --template vue cd turfjs-spatial-relation 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 classspatial-validator el-card !-- 头部标题 -- div classheader h2空间关系校验配送范围/路径/邻近/h2 div classdesc 基于 Turf.js 实现点在面内判断、路径与区域相交检测、以及最近邻要素查询。 /div /div !-- 1. 配送区域设置面要素编辑 -- el-collapse v-modelactiveNames el-collapse-item title配送区域设置 (Polygon) name1 div classzone-editor div classcoords-list !-- 区域坐标点列表 -- div v-for(coord, index) in zoneCoords :keyindex classcoord-row span点 {{ index 1 }}:/span el-input-number v-modelcoord[0] :step0.01 sizesmall controls-positionright placeholder经度 / el-input-number v-modelcoord[1] :step0.01 sizesmall controls-positionright placeholder纬度 / el-button typedanger iconDelete circle sizesmall clickremoveZoneCoord(index) v-ifzoneCoords.length 3 / /div !-- 区域操作按钮 -- el-button typeprimary sizesmall clickaddZoneCoord添加节点/el-button el-button typewarning sizesmall clickresetZone重置为默认区域/el-button /div !-- 区域有效性提示 -- div classzone-preview p 当前区域状态: el-tag :typeisValidZone ? success : danger {{ isValidZone ? 有效 : 无效 (需首尾闭合且至少3点) }} /el-tag /p /div /div /el-collapse-item /el-collapse el-divider / !-- 2. 空间关系分析标签页 -- el-tabs typeborder-card v-modelactiveTab !-- 2.1 点在面内判断 -- el-tab-pane label点在面内判断 namepoint div classoperation-panel div classinput-group span测试点坐标 (点击地图选取):/span el-input-number v-modeltestPoint[0] :step0.001 placeholder经度 / el-input-number v-modeltestPoint[1] :step0.001 placeholder纬度 / el-button typeprimary clickcheckPointInPolygon校验/el-button /div !-- 点在面内结果 -- div classresult-box v-ifpointResult ! null 结果: el-tag :typepointResult ? success : danger sizelarge {{ pointResult ? 在区域内 (Inside) : 在区域外 (Outside) }} /el-tag /div /div /el-tab-pane !-- 2.2 路径相交判断 -- el-tab-pane label路径相交判断 nameroute div classoperation-panel p输入路径点 (LineString) - 点击地图添加节点:/p !-- 路径坐标点列表 -- div v-for(pt, idx) in routeCoords :keyr idx classcoord-row span节点 {{ idx 1 }}:/span el-input-number v-modelpt[0] :step0.001 sizesmall placeholder经度 / el-input-number v-modelpt[1] :step0.001 sizesmall placeholder纬度 / el-button v-ifrouteCoords.length 2 typetext clickremoveRouteCoord(idx)删除/el-button /div !-- 路径操作按钮 -- div classactions el-button sizesmall clickaddRouteCoord添加路径点/el-button el-button typeprimary clickcheckRouteIntersect分析路径/el-button /div !-- 路径分析结果 -- div classresult-box v-ifrouteResult p 相交情况: el-tag typeinfo{{ routeResult.crosses ? 相交 (Crosses) : 不相交 }}/el-tag /p p 包含情况: el-tag typewarning{{ routeResult.within ? 完全在区域内 : 部分或完全在区域外 }}/el-tag /p /div /div /el-tab-pane !-- 2.3 最近邻查询 -- el-tab-pane label最近邻查询 (Nearest) namenearest div classoperation-panel p参考点集合 (例如附近的骑手):/p !-- 骑手点列表 -- div classpoi-list span v-for(poi, idx) in poiList :keyp idx classpoi-tag 骑手{{ idx 1 }} [{{ poi[0] }}, {{ poi[1] }}] /span /div !-- 目标位置输入 -- div classinput-group span目标位置 (点击地图选取):/span el-input-number v-modeltargetPoint[0] :step0.001 placeholder经度 / el-input-number v-modeltargetPoint[1] :step0.001 placeholder纬度 / el-button typesuccess clickfindNearest查找最近骑手/el-button /div !-- 最近邻查询结果 -- div classresult-box v-ifnearestResult 最近要素: strong骑手 {{ nearestResult.index 1 }}/strongbr / 坐标: [{{ nearestResult.coord[0] }}, {{ nearestResult.coord[1] }}]br / 距离: {{ nearestResult.distance.toFixed(3) }} km /div /div /el-tab-pane /el-tabs !-- 地图可视化区域 -- div classmap-container div idspatial-map classmap-view/div /div /el-card /div /template script setup import { ref, reactive, onMounted, watch, computed } from vue; import L from leaflet; import leaflet/dist/leaflet.css; import * as turf from turf/turf; // --- 1. 状态管理 --- const activeNames ref([1]); // 折叠面板默认展开 const activeTab ref(point); // 默认选中点在面内标签页 // 配送区域坐标默认北京某区域首尾闭合 const defaultZone [ [116.38, 39.9], [116.42, 39.9], [116.42, 39.92], [116.38, 39.92], [116.38, 39.9], // 首尾闭合 ]; const zoneCoords ref(JSON.parse(JSON.stringify(defaultZone))); // 点在面内测试数据 const testPoint ref([116.4, 39.91]); // 默认配送区域中心点 const pointResult ref(null); // 点在面内结果 // 路径分析数据 const routeCoords ref([ [116.37, 39.91], [116.43, 39.91], ]); // 默认横穿配送区域的路径 const routeResult ref(null); // 路径分析结果 // 最近邻查询数据骑手点集合 const poiList ref([ [116.39, 39.89], [116.41, 39.93], [116.43, 39.9], ]); const targetPoint ref([116.4, 39.91]); // 默认目标位置 const nearestResult ref(null); // 最近邻查询结果 // --- 2. 地图实例管理 --- let map null; let zoneLayer null; // 配送区域图层 let testPointLayer null; // 测试点图层 let routeLayer null; // 路径图层 let poiLayerGroup null; // 骑手点图层组 let nearestLineLayer null; // 最近邻连接线图层 // --- 3. 生命周期与监听 --- onMounted(() { initMap(); // 初始化地图 updateMapViz(); // 初始化地图可视化 }); // 监听配送区域变化更新地图并清空结果 watch( zoneCoords, () { updateMapViz(); pointResult.value null; routeResult.value null; }, { deep: true } ); // 监听测试点变化更新地图并清空结果 watch( testPoint, () { updateTestPointViz(); pointResult.value null; }, { deep: true } ); // 监听路径变化更新地图并清空结果 watch( routeCoords, () { updateRouteViz(); routeResult.value null; }, { deep: true } ); // 监听骑手点/目标位置变化更新地图并清空结果 watch( [poiList, targetPoint], () { updatePoiViz(); nearestResult.value null; }, { deep: true } ); // --- 4. 配送区域操作逻辑 --- // 校验配送区域有效性首尾闭合 至少4个点 const isValidZone computed(() { if (zoneCoords.value.length 4) return false; // 面要素至少3个点闭合点4个点 const first zoneCoords.value[0]; const last zoneCoords.value[zoneCoords.value.length - 1]; return first[0] last[0] first[1] last[1]; // 首尾坐标一致 }); // 添加配送区域节点在倒数第二个点后插入 function addZoneCoord() { const len zoneCoords.value.length; if (len 0) { const last zoneCoords.value[len - 2]; // 倒数第二个点避免修改闭合点 zoneCoords.value.splice(len - 1, 0, [last[0] 0.005, last[1] 0.005]); } else { zoneCoords.value.push([116.4, 39.9]); } } // 删除配送区域节点 function removeZoneCoord(index) { zoneCoords.value.splice(index, 1); } // 重置配送区域为默认值 function resetZone() { zoneCoords.value JSON.parse(JSON.stringify(defaultZone)); } // --- 5. 核心功能1点在面内判断 --- function checkPointInPolygon() { if (!isValidZone.value) { ElMessage.warning(配送区域无效请先设置有效闭合区域); return; } // 创建Turf.js点要素和面要素 const pt turf.point(testPoint.value); const poly turf.polygon([zoneCoords.value]); // 核心API判断点是否在面内支持含孔洞的面、边界判断 const isInside turf.booleanPointInPolygon(pt, poly); pointResult.value isInside; } // --- 6. 核心功能2路径相交判断 --- // 添加路径节点 function addRouteCoord() { const last routeCoords.value[routeCoords.value.length - 1]; routeCoords.value.push([last[0] 0.01, last[1]]); } // 删除路径节点 function removeRouteCoord(index) { routeCoords.value.splice(index, 1); } // 分析路径与配送区域的关系 function checkRouteIntersect() { if (!isValidZone.value || routeCoords.value.length 2) { ElMessage.warning(配送区域无效或路径点不足2个); return; } // 创建Turf.js线要素和面要素 const line turf.lineString(routeCoords.value); const poly turf.polygon([zoneCoords.value]); // 核心API1判断线是否与面相交 const crosses turf.booleanCrosses(line, poly); // 核心API2判断线是否完全在面内 const within turf.booleanWithin(line, poly); routeResult.value { crosses, within }; } // --- 7. 核心功能3最近邻查询 --- function findNearest() { // 创建Turf.js目标点要素和骑手点要素集合 const target turf.point(targetPoint.value); const points turf.featureCollection(poiList.value.map((coord) turf.point(coord))); // 核心API1查找最近的点要素 const nearest turf.nearestPoint(target, points); // 核心API2计算两点间距离默认单位千米 const dist turf.distance(target, nearest); // 查找最近骑手在原始列表中的索引 const index poiList.value.findIndex( (p) p[0] nearest.geometry.coordinates[0] p[1] nearest.geometry.coordinates[1] ); // 存储查询结果 nearestResult.value { index, coord: nearest.geometry.coordinates, distance: dist, }; // 绘制最近邻连接线绿色虚线 if (nearestLineLayer) map.removeLayer(nearestLineLayer); nearestLineLayer L.polyline( [ [targetPoint.value[1], targetPoint.value[0]], // Leaflet坐标[lat, lng] [nearest.geometry.coordinates[1], nearest.geometry.coordinates[0]], ], { color: green, dashArray: 5, 5, weight: 2 } ).addTo(map); } // --- 8. 地图初始化与可视化 --- // 初始化Leaflet地图 function initMap() { // 创建地图实例中心坐标39.91°N, 116.4°E缩放级别12 map L.map(spatial-map).setView([39.91, 116.4], 12); // 加载OpenStreetMap底图瓦片 L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: copy; OpenStreetMap contributors, }).addTo(map); // 初始化骑手点图层组 poiLayerGroup L.layerGroup().addTo(map); // 地图点击事件根据当前标签页设置对应坐标 map.on(click, (e) { const { lat, lng } e.latlng; // 转换坐标格式Leaflet [lat, lng] → Turf.js [lng, lat] const coords [Number(lng.toFixed(5)), Number(lat.toFixed(5))]; if (activeTab.value point) { testPoint.value coords; // 点在面内设置测试点 } else if (activeTab.value route) { routeCoords.value.push(coords); // 路径分析添加路径点 } else if (activeTab.value nearest) { targetPoint.value coords; // 最近邻设置目标位置 } }); } // 更新地图可视化配送区域 function updateMapViz() { if (!map) return; // 移除旧的配送区域图层 if (zoneLayer) map.removeLayer(zoneLayer); // 绘制新的配送区域仅当区域有效时 if (isValidZone.value) { // 转换坐标格式Turf.js [lng, lat] → Leaflet [lat, lng] const latLngs zoneCoords.value.map((c) [c[1], c[0]]); zoneLayer L.polygon(latLngs, { color: blue, fillOpacity: 0.1, weight: 2, }).addTo(map); } // 更新其他图层 updateTestPointViz(); updateRouteViz(); updatePoiViz(); } // 更新测试点可视化 function updateTestPointViz() { if (!map) return; if (testPointLayer) map.removeLayer(testPointLayer); // 绘制测试点红色圆形标记 testPointLayer L.circleMarker([testPoint.value[1], testPoint.value[0]], { color: red, radius: 6, fillOpacity: 0.8, }) .addTo(map) .bindPopup(测试点); } // 更新路径可视化 function updateRouteViz() { if (!map) return; if (routeLayer) map.removeLayer(routeLayer); // 绘制路径橙色折线 if (routeCoords.value.length 1) { const latLngs routeCoords.value.map((c) [c[1], c[0]]); routeLayer L.polyline(latLngs, { color: orange, weight: 4 }).addTo(map); } } // 更新骑手点和目标位置可视化 function updatePoiViz() { if (!map) return; poiLayerGroup.clearLayers(); // 清空旧图层 // 绘制目标位置绿色圆形标记 L.circleMarker([targetPoint.value[1], targetPoint.value[0]], { color: green, radius: 8, fillOpacity: 0.8, }) .addTo(poiLayerGroup) .bindPopup(目标位置); // 绘制骑手点默认标记 poiList.value.forEach((p, i) { L.marker([p[1], p[0]]) .addTo(poiLayerGroup) .bindPopup(骑手 ${i 1}); }); } /script style scoped .spatial-validator { margin: 20px; } .header { margin-bottom: 15px; } .header h2 { font-size: 18px; font-weight: 600; color: #333; margin: 0 0 5px 0; } .header .desc { font-size: 14px; color: #666; } .zone-editor { background: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 10px; } .coords-list { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 10px; } .coord-row { display: flex; align-items: center; gap: 8px; padding: 5px; background: #fff; border-radius: 4px; } .coord-row span { font-size: 13px; color: #333; } .zone-preview { margin-top: 10px; font-size: 13px; } .operation-panel { padding: 15px; } .input-group { display: flex; gap: 10px; align-items: center; margin-bottom: 15px; flex-wrap: wrap; } .input-group span { font-size: 14px; color: #333; } .actions { margin: 10px 0; display: flex; gap: 10px; } .map-container { height: 400px; margin-top: 20px; border: 1px solid #ddd; border-radius: 6px; overflow: hidden; } .map-view { width: 100%; height: 100%; } .result-box { margin-top: 10px; padding: 12px; background: #eef5ff; border-radius: 6px; font-size: 14px; } .poi-list { margin-bottom: 10px; flex-wrap: wrap; display: flex; gap: 5px; } .poi-tag { display: inline-block; background: #f0f2f5; padding: 4px 8px; margin-right: 5px; border-radius: 4px; font-size: 13px; color: #333; } /style2. 核心代码深度解析1Turf.js 核心 API 详解空间关系判断重点API作用关键参数说明booleanPointInPolygon(point, polygon)判断点是否在面内-point点要素-polygon面要素- 支持边界判断、含孔洞的面要素booleanCrosses(line, polygon)判断线是否与面相交-line线要素-polygon面要素- 返回true表示线穿过面边界booleanWithin(line, polygon)判断线是否完全在面内-line线要素-polygon面要素- 返回true表示线无部分超出面nearestPoint(target, points)查找最近的点要素-target目标点要素-points点要素集合FeatureCollectiondistance(point1, point2)计算两点间大地距离- 单位默认千米kilometers- 支持通过{ units: meters }切换单位2核心逻辑拆解点在面内判断核心外卖配送场景中用户下单后需判断用户位置是否在商家配送范围内核心依赖booleanPointInPolygonAPI。代码中先校验配送区域有效性首尾闭合再创建 Turf.js 点 / 面要素调用 API 返回布尔结果最终在界面和地图上可视化展示。路径相交判断核心配送路线规划中需判断路线是否穿过禁行区域 / 配送区域核心依赖booleanCrosses是否相交和booleanWithin是否完全包含。代码中先创建线要素配送路线和面要素配送区域调用两个 API 获取结果区分 “相交”“完全包含”“完全不相交” 三种场景。最近邻查询核心外卖派单场景中需查找离用户最近的骑手核心依赖nearestPoint找最近点和distance算距离。代码中先将骑手点转换为 FeatureCollection调用nearestPoint找到最近骑手再用distance计算两者间距离最后在地图上绘制绿色虚线连接线直观展示匹配结果。地图交互优化坐标格式转换Turf.js 使用[lng, lat]经前纬后Leaflet 使用[lat, lng]纬前经后代码中通过map((c) [c[1], c[0]])完成转换点击选点监听地图点击事件根据当前标签页自动填充对应坐标测试点 / 路径点 / 目标位置提升操作便捷性图层管理为不同要素创建独立图层支持动态更新和移除避免图层重叠导致的显示异常。3关键注意事项面要素闭合要求booleanPointInPolygon/booleanCrosses等 API 要求面要素必须首尾闭合代码中通过isValidZone计算属性校验避免无效面要素导致的计算错误。坐标精度控制地图点击选点时通过toFixed(5)限制坐标精度为 5 位小数约 1 米精度既满足场景需求又避免浮点数冗余。最近邻查询边界处理nearestPoint返回的是要素集合中距离最近的点若多个点距离相同返回第一个匹配点代码中通过findIndex查找原始列表索引确保结果与界面展示的骑手编号一致。四、功能效果演示1. 操作流程配送区域设置展开 “配送区域设置” 面板可添加 / 删除节点、重置默认区域系统自动校验区域有效性首尾闭合 至少 3 点无效时提示红色标签。点在面内判断在 “点在面内判断” 标签页手动输入测试点坐标或点击地图选点点击 “校验”界面显示 “在区域内 / 外” 结果地图上红色标记为测试点蓝色区域为配送范围。路径相交判断在 “路径相交判断” 标签页添加 / 删除路径点或点击地图选点点击 “分析路径”界面显示 “相交 / 不相交”“完全在区域内 / 外” 结果地图上橙色折线为路径。最近邻查询在 “最近邻查询” 标签页手动输入目标位置或点击地图选点点击 “查找最近骑手”界面显示最近骑手编号、坐标、距离地图上绿色虚线连接目标位置与最近骑手。2. 示例场景输出点在面内测试点[116.4, 39.91]配送区域中心→ 结果在区域内测试点[116.37, 39.91]区域外→ 结果在区域外。路径相交路径[[116.37, 39.91], [116.43, 39.91]]横穿配送区域→ 结果相交、部分在区域外。最近邻查询目标位置[116.4, 39.91]→ 最近骑手骑手 1坐标[116.39, 39.89]距离≈0.223 千米。五、代码仓库地址完整代码已上传至 Gitee可直接克隆运行https://gitee.com/tang-yunyan-syp/turfjs-vue3-demo.git六、实战拓展方向支持更多空间关系扩展booleanContains面包含点 / 线、booleanOverlap面重叠、booleanDisjoint无交集等 API覆盖全量空间关系判断。批量校验支持上传多个测试点 / 路径批量判断空间关系并导出结果Excel/CSV。地理编码集成结合高德 / 百度地图地理编码 API支持通过地址如 “北京市朝阳区 XX 路”生成坐标无需手动输入经纬度。动态配送区域支持在地图上拖拽编辑配送区域节点实时更新区域范围。多区域叠加判断支持设置多个配送区域判断点 / 路径与多个区域的空间关系如 “是否在任意配送区域内”。距离阈值过滤最近邻查询时添加距离阈值如仅查找 1 千米内的骑手过滤超出范围的要素。七、常见问题排查点在面内判断结果异常原因面要素未闭合、坐标顺序错误经纬度写反解决方案检查isValidZone状态确保面要素首尾闭合坐标格式为[lng, lat]。路径相交判断无结果原因路径点不足 2 个、配送区域无效解决方案至少添加 2 个路径点确保配送区域有效首尾闭合 至少 3 点。最近邻查询结果错误原因骑手点集合为空、目标位置坐标无效解决方案检查poiList是否有有效坐标目标位置经纬度为数字且在合理范围经度 ±180纬度 ±90。地图要素显示偏移原因国内底图高德 / 百度使用 GCJ-02 坐标系而 OpenStreetMap 使用 WGS84 坐标系解决方案集成坐标转换库如coordtransform将 WGS84 坐标转换为 GCJ-02 后再渲染。八、专栏地址本文已同步至 CSDN 专栏可查看更多 Turf.js 实战内容https://blog.csdn.net/m0_72065108/article/details/155226062?spm1001.2014.3001.5501