You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于经纬度的GeoJSON多边形Canvas绘制及内缩多边形实现问题

问题:GeoJSON多边形内缩偏移时边角坐标异常的修复方案

需求概述

  • 编写函数接收包含lat(纬度)、lon(经度)的GeoJSON多边形数据,在2D Canvas上绘制原多边形
  • 按北、东、西、南四个方向的不同偏移距离,绘制内缩后的多边形

现存问题

当前实现中,部分边角(绿色圈标注位置)出现坐标维度异常:

  • 仅需单方向偏移的边角处理逻辑错误
  • 需要同时处理双方向偏移的场景未正确计算偏移向量,导致内缩多边形边角位置偏离预期,绘制结果错误

效果对比

  • 错误效果:
    错误效果
  • 正确效果:
    正确效果

问题根源

内缩多边形的边角计算未考虑相邻边的几何关系:

  1. 仅对顶点做单一方向偏移,未通过相邻边的平移交点来确定边角位置
  2. 未根据边的走向匹配对应方向的偏移值,导致单方向偏移场景处理错误
  3. 忽略了凸角/凹角的内法线方向差异,偏移方向未始终指向多边形内部

修复方案

核心思路

  1. 先将GeoJSON经纬度坐标转换为Canvas屏幕坐标(可采用线性映射或墨卡托投影)
  2. 对每个顶点,计算相邻两条边的内法线向量,根据边的走向匹配对应方向的偏移距离
  3. 平移相邻边并求交点,以此作为内缩后的边角坐标;若无法获取交点,采用合并法线向量的 fallback 方案
  4. 按计算后的内缩顶点绘制多边形

示例代码

// 经纬度转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 };
}

关键修复点

  1. 边角交点计算:通过平移相邻边并求交点确定内缩边角坐标,解决双方向偏移场景的错误
  2. 方向偏移匹配:根据边的水平/垂直走向匹配对应方向的偏移值,修正单方向偏移的逻辑
  3. 内法线向量:确保偏移方向始终指向多边形内部,适配凸角与凹角的不同情况

内容的提问来源于stack exchange,提问作者zizu 112233

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 13:39:56