基于经纬度的GeoJSON多边形Canvas绘制及内缩多边形实现问题
问题:GeoJSON多边形内缩偏移时边角坐标异常的修复方案
需求概述
- 编写函数接收包含
lat(纬度)、lon(经度)的GeoJSON多边形数据,在2D Canvas上绘制原多边形 - 按北、东、西、南四个方向的不同偏移距离,绘制内缩后的多边形
现存问题
当前实现中,部分边角(绿色圈标注位置)出现坐标维度异常:
- 仅需单方向偏移的边角处理逻辑错误
- 需要同时处理双方向偏移的场景未正确计算偏移向量,导致内缩多边形边角位置偏离预期,绘制结果错误
效果对比
- 错误效果:
- 正确效果:
问题根源
内缩多边形的边角计算未考虑相邻边的几何关系:
- 仅对顶点做单一方向偏移,未通过相邻边的平移交点来确定边角位置
- 未根据边的走向匹配对应方向的偏移值,导致单方向偏移场景处理错误
- 忽略了凸角/凹角的内法线方向差异,偏移方向未始终指向多边形内部
修复方案
核心思路
- 先将GeoJSON经纬度坐标转换为Canvas屏幕坐标(可采用线性映射或墨卡托投影)
- 对每个顶点,计算相邻两条边的内法线向量,根据边的走向匹配对应方向的偏移距离
- 平移相邻边并求交点,以此作为内缩后的边角坐标;若无法获取交点,采用合并法线向量的 fallback 方案
- 按计算后的内缩顶点绘制多边形
示例代码
// 经纬度转Canvas屏幕坐标(线性映射示例,可替换为墨卡托投影) function latLonToScreen(lat, lon, canvasWidth, canvasHeight, bounds) { const { minLon, maxLon, minLat, maxLat } = bounds; const x = ((lon - minLon) / (maxLon - minLon)) * canvasWidth; // Canvas原点在左上角,需反转纬度映射 const y = ((maxLat - lat) / (maxLat - minLat)) * canvasHeight; return { x, y }; } // 计算两条直线的交点 function getLineIntersection(line1Start, line1End, line2Start, line2End) { const x1 = line1Start.x, y1 = line1Start.y; const x2 = line1End.x, y2 = line1End.y; const x3 = line2Start.x, y3 = line2Start.y; const x4 = line2End.x, y4 = line2End.y; const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (denom === 0) return null; // 平行或重合 const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { return { x: x1 + t * (x2 - x1), y: y1 + t * (y2 - y1) }; } return null; } // 计算内缩多边形顶点 function calculateOffsetPolygon(vertices, northOffset, eastOffset, southOffset, westOffset) { const offsetVertices = []; const n = vertices.length; for (let i = 0; i < n; i++) { const curr = vertices[i]; const prev = vertices[(i - 1 + n) % n]; const next = vertices[(i + 1) % n]; // 计算前后边的方向向量 const prevVec = { x: curr.x - prev.x, y: curr.y - prev.y }; const nextVec = { x: next.x - curr.x, y: next.y - curr.y }; // 计算内法线向量(指向多边形内部) const prevNormal = { x: prevVec.y, y: -prevVec.x }; const nextNormal = { x: nextVec.y, y: -nextVec.x }; // 归一化法线向量 const prevLen = Math.hypot(prevNormal.x, prevNormal.y); const nextLen = Math.hypot(nextNormal.x, nextNormal.y); const normalizedPrevNormal = { x: prevNormal.x / prevLen, y: prevNormal.y / prevLen }; const normalizedNextNormal = { x: nextNormal.x / nextLen, y: nextNormal.y / nextLen }; // 根据边的走向匹配偏移距离 let prevOffsetDist = 0; let nextOffsetDist = 0; // 处理前一条边的偏移 if (Math.abs(prevVec.x) > Math.abs(prevVec.y)) { // 近似水平边:东/西偏移 prevOffsetDist = prevVec.x > 0 ? westOffset : eastOffset; } else { // 近似垂直边:北/南偏移 prevOffsetDist = prevVec.y > 0 ? northOffset : southOffset; } // 处理后一条边的偏移 if (Math.abs(nextVec.x) > Math.abs(nextVec.y)) { nextOffsetDist = nextVec.x > 0 ? westOffset : eastOffset; } else { nextOffsetDist = nextVec.y > 0 ? northOffset : southOffset; } // 平移前后两条边 const prevLineStart = { x: prev.x + normalizedPrevNormal.x * prevOffsetDist, y: prev.y + normalizedPrevNormal.y * prevOffsetDist }; const prevLineEnd = { x: curr.x + normalizedPrevNormal.x * prevOffsetDist, y: curr.y + normalizedPrevNormal.y * prevOffsetDist }; const nextLineStart = { x: curr.x + normalizedNextNormal.x * nextOffsetDist, y: curr.y + normalizedNextNormal.y * nextOffsetDist }; const nextLineEnd = { x: next.x + normalizedNextNormal.x * nextOffsetDist, y: next.y + normalizedNextNormal.y * nextOffsetDist }; // 获取平移后边的交点作为内缩顶点 const intersection = getLineIntersection(prevLineStart, prevLineEnd, nextLineStart, nextLineEnd); if (intersection) { offsetVertices.push(intersection); } else { // Fallback:合并法线向量后平移当前顶点 const combinedNormal = { x: normalizedPrevNormal.x + normalizedNextNormal.x, y: normalizedPrevNormal.y + normalizedNextNormal.y }; const combinedLen = Math.hypot(combinedNormal.x, combinedNormal.y); const normalizedCombined = { x: combinedNormal.x / combinedLen, y: combinedNormal.y / combinedLen }; const avgOffset = (prevOffsetDist + nextOffsetDist) / 2; offsetVertices.push({ x: curr.x + normalizedCombined.x * avgOffset, y: curr.y + normalizedCombined.y * avgOffset }); } } return offsetVertices; } // 在Canvas绘制多边形 function drawPolygon(canvasCtx, vertices, fillStyle, strokeStyle) { canvasCtx.fillStyle = fillStyle; canvasCtx.strokeStyle = strokeStyle; canvasCtx.beginPath(); canvasCtx.moveTo(vertices[0].x, vertices[0].y); vertices.slice(1).forEach(v => canvasCtx.lineTo(v.x, v.y)); canvasCtx.closePath(); canvasCtx.fill(); canvasCtx.stroke(); } // 主函数:处理GeoJSON并绘制原多边形与内缩多边形 function processGeoJSONAndDraw(geoJSON, canvas, northOffset, eastOffset, southOffset, westOffset) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); // 提取GeoJSON坐标并转换为屏幕坐标 const coordinates = geoJSON.features[0].geometry.coordinates[0]; const bounds = calculateBounds(coordinates); const screenVertices = coordinates.map(coord => latLonToScreen(coord[1], coord[0], canvas.width, canvas.height, bounds)); // 绘制原多边形 drawPolygon(ctx, screenVertices, 'rgba(0, 100, 200, 0.3)', 'rgba(0, 100, 200, 1)'); // 计算并绘制内缩多边形 const offsetVertices = calculateOffsetPolygon(screenVertices, northOffset, eastOffset, southOffset, westOffset); drawPolygon(ctx, offsetVertices, 'rgba(200, 100, 0, 0.3)', 'rgba(200, 100, 0, 1)'); } // 计算GeoJSON坐标的边界范围 function calculateBounds(coordinates) { let minLon = Infinity, maxLon = -Infinity; let minLat = Infinity, maxLat = -Infinity; coordinates.forEach(([lon, lat]) => { minLon = Math.min(minLon, lon); maxLon = Math.max(maxLon, lon); minLat = Math.min(minLat, lat); maxLat = Math.max(maxLat, lat); }); return { minLon, maxLon, minLat, maxLat }; }
关键修复点
- 边角交点计算:通过平移相邻边并求交点确定内缩边角坐标,解决双方向偏移场景的错误
- 方向偏移匹配:根据边的水平/垂直走向匹配对应方向的偏移值,修正单方向偏移的逻辑
- 内法线向量:确保偏移方向始终指向多边形内部,适配凸角与凹角的不同情况
内容的提问来源于stack exchange,提问作者zizu 112233
相关产品推荐
相关产品推荐

