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

如何解决amCharts 5地图投影中Rhumb Line的环绕问题?

解决amCharts 5恒向线跨地图边缘环绕问题

核心问题

当使用panX: "rotateX"的矩形投影(如墨卡托)时,amCharts默认会自动将跨180°经线的线段环绕绘制,导致恒向线错误地绕世界一圈。需要禁用自动环绕,并确保跨边缘的线段被拆分为独立的不连续段。

修改方案

  1. 禁用线的自动环绕:给MapLineSeries设置noWrap: true,阻止amCharts自动连接跨边缘的点。
  2. 优化恒向线拆分逻辑:将穿过180°经线的线段拆分为多个独立的LineString,而不是在同一条线中插入-180/180的点,确保每个线段都在可视范围内。

修改后的完整代码

JavaScript

am5.ready(function() {

  // Create root and chart
  var root = am5.Root.new("chartdiv");
  var chart = root.container.children.push(
    am5map.MapChart.new(root, {
      panX: "rotateX",
      panY: "none",
      projection: am5map.geoNaturalEarth1()
    })
  );

  // Set themes
  root.setThemes([
    am5themes_Animated.new(root)
  ]);

  // Create polygon series
  var polygonSeries = chart.series.push(
    am5map.MapPolygonSeries.new(root, {
      geoJSON: am5geodata_worldLow
    })
  );

  var graticuleSeries = chart.series.insertIndex(
    0, am5map.GraticuleSeries.new(root, {})
  );

  graticuleSeries.mapLines.template.setAll({
    stroke: am5.color(0x000000),
    strokeOpacity: 0.1
  });

  var backgroundSeries = chart.series.unshift(
    am5map.MapPolygonSeries.new(root, {})
  );

  backgroundSeries.mapPolygons.template.setAll({
    fill: am5.color(0xedf7fa),
    stroke: am5.color(0xedf7fa),
  });

  backgroundSeries.data.push({
    geometry: am5map.getGeoRectangle(90, 180, -90, -180)
  });

  setProjection("geoAiry");

  function setProjection(name) {
    chart.set("projection", d3[name].call(this));
    setButtonState();
    // 重新绘制恒向线
    redrawRhumbLine();
  }

  function setIndex(offset) {
    var selector = document.getElementById("selector");
    var index = selector.selectedIndex + offset;
    if (index < 0) {
      index = 0;
    }
    if (index > (selector.options.length - 1)) {
      index = selector.options.length - 1;
    }
    selector.selectedIndex = index;
    setProjection(selector.options[index].value);
    setButtonState();
  }

  function setButtonState() {
    var selector = document.getElementById("selector");
    var index = selector.selectedIndex;
    if (index == 0) {
      document.getElementById("selector-prev").disabled = "disabled";
      document.getElementById("selector-next").disabled = "";
    } else if (index >= (selector.options.length - 1)) {
      document.getElementById("selector-prev").disabled = "";
      document.getElementById("selector-next").disabled = "disabled";
    } else {
      document.getElementById("selector-prev").disabled = "";
      document.getElementById("selector-next").disabled = "";
    }
  }

  // Create a line series with noWrap enabled
  var lineSeries = chart.series.push(am5map.MapLineSeries.new(root, {
    lineType: "straight",
    noWrap: true // 禁用自动环绕
  }));
  lineSeries.stroke = am5.color("#FF0000");

  // Functions to calculate Rhumb Line segments
  function toRad(x) {
    return x * Math.PI / 180;
  }

  function toDeg(x) {
    return x * 180 / Math.PI;
  }

  function calculateRhumbLineSegments(start, end, numPoints = 100) {
    let segments = [];
    let currentSegment = [];
    
    let lat1 = toRad(start.latitude);
    let lon1 = toRad(start.longitude);
    let lat2 = toRad(end.latitude);
    let lon2 = toRad(end.longitude);

    let dLon = lon2 - lon1;
    let dPhi = Math.log(Math.tan(lat2 / 2 + Math.PI / 4) / Math.tan(lat1 / 2 + Math.PI / 4));
    if (Math.abs(lon2 - lon1) > Math.PI) {
      if (lon2 <= lon1) {
        dLon = ((lon2 + 2 * Math.PI) - lon1);
      } else {
        dLon = ((lon2 - 2 * Math.PI) - lon1);
      }
    }

    // Normalize to -180..+180
    lon1 = (lon1 + 3 * Math.PI) % (2 * Math.PI) - Math.PI;
    lon2 = (lon2 + 3 * Math.PI) % (2 * Math.PI) - Math.PI;

    let lastLon = lon1;
    // 添加起点到当前线段
    currentSegment.push({
      latitude: toDeg(lat1),
      longitude: toDeg(lon1)
    });

    for (let i = 1; i <= numPoints; i++) {
      let f = i / numPoints;
      let lat = lat1 + (lat2 - lat1) * f;
      let lon = lon1 + dLon * f;
      let degLon = toDeg(lon);
      let degLat = toDeg(lat);

      // 检测是否跨180度经线
      if (Math.abs(lon - lastLon) > Math.PI) {
        // 计算跨边缘的中间点(分别在-180和180处)
        let midLat = toDeg((lat + toRad(currentSegment[currentSegment.length - 1].latitude)) / 2);
        
        // 结束当前线段在边缘处
        if (lon > lastLon) {
          currentSegment.push({ latitude: midLat, longitude: -180 });
        } else {
          currentSegment.push({ latitude: midLat, longitude: 180 });
        }
        segments.push(currentSegment);
        
        // 开始新的线段在对面边缘
        currentSegment = [];
        if (lon > lastLon) {
          currentSegment.push({ latitude: midLat, longitude: 180 });
        } else {
          currentSegment.push({ latitude: midLat, longitude: -180 });
        }
      }

      currentSegment.push({ latitude: degLat, longitude: degLon });
      lastLon = lon;
    }

    // 添加最后一个线段
    segments.push(currentSegment);
    return segments;
  }

  function redrawRhumbLine() {
    // 清除之前的线
    lineSeries.data.clear();
    
    // Calculate Rhumb Line segments from New York to London
    var newYork = {
      latitude: 40.7128,
      longitude: -74.0060
    };
    var london = {
      latitude: 51.5074,
      longitude: -0.1278
    };
    var rhumbLineSegments = calculateRhumbLineSegments(newYork, london);

    // 为每个线段创建独立的线
    rhumbLineSegments.forEach(function(segment) {
      if (segment.length >= 2) {
        var coordinates = segment.map(point => [point.longitude, point.latitude]);
        lineSeries.pushDataItem({
          geometry: {
            type: "LineString",
            coordinates: coordinates
          }
        });
      }
    });
  }

  // 初始化绘制
  redrawRhumbLine();

  // Expose the functions to the global scope
  window.setProjection = setProjection;
  window.setIndex = setIndex;

}); // end am5.ready()

CSS

.tools {
  text-align: center;
}

.tools select,
.tools input {
  font-size: 1.2em;
  padding: 0.2em 0.4em;
}

#chartdiv {
  width: 100%;
  height: 500px;
}

HTML

<div class="tools">
  <input id="selector-prev" type="button" value="&lt;" onclick="setIndex(-1);" disabled="disabled" />
  <select id="selector" onchange="setProjection(this.options[this.selectedIndex].value);">
    <option value="geoAiry">d3.geoAiry()</option>
    <option value="geoMercator">d3.geoMercator()</option>
  </select>
  <input id="selector-next" type="button" value="&gt;" onclick="setIndex(1);" />
</div>
<div id="chartdiv"></div>
<script src="https://cdn.amcharts.com/lib/5/index.js"></script>
<script src="https://cdn.amcharts.com/lib/5/map.js"></script>
<script src="https://cdn.amcharts.com/lib/5/geodata/worldLow.js"></script>
<script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
<script src="https://d3js.org/d3-array.v1.min.js"></script>
<script src="https://d3js.org/d3-geo.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>

关键改动说明

  • noWrap: true:添加到MapLineSeries配置中,告诉amCharts不要自动将跨180°经线的线段连接成环绕世界的线。
  • 拆分线段为独立数组:calculateRhumbLineSegments函数不再返回单个点数组,而是返回多个线段数组,每个线段都是不跨180°的连续点集,确保每个线段在地图上独立显示,不会被错误连接。
  • 重新绘制逻辑:添加redrawRhumbLine函数,切换投影时重新绘制恒向线,保证投影变化后线段正确显示。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 04:17:03