1

I have adapted an amCharts 5 example (https://www.amcharts.com/demos/map-using-d3-projections/) with the aim of plotting a Rhumb Line on the map projections. I managed to plot a Rhumb Line. However, using a rectangular projection such as Mercator, if you drag left/right to the point where the Rhumb Line crosses the edge of the map, the Rhumb Line needlessly spans the wrong way 'around the world'. Easier to see it in action that it is to explain it! How do I get Rhumb Lines to simply stay split when they cross the edge of a map and avoid the wraparound problem?

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();
  }

  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
  var lineSeries = chart.series.push(am5map.MapLineSeries.new(root, {
    lineType: "straight" // Set line type to straight for Rhumb Lines
  }));
  lineSeries.stroke = am5.color("#FF0000"); // Set the line color to red

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

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

  function calculateRhumbLinePoints(start, end, numPoints = 100) {
    let points = [];
    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;
    for (let i = 0; i <= numPoints; i++) {
      let f = i / numPoints;
      let lat = lat1 + (lat2 - lat1) * f;
      let lon = lon1 + dLon * f;

      // If the line crosses the 180° meridian, split it into two segments
      if (Math.abs(lon - lastLon) > Math.PI) {
        let midLat = (lat + toRad(points[points.length - 1].latitude)) / 2;
        if (lon > lastLon) {
          points.push({
            latitude: toDeg(midLat),
            longitude: -180
          });
          points.push({
            latitude: toDeg(midLat),
            longitude: 180
          });
        } else {
          points.push({
            latitude: toDeg(midLat),
            longitude: 180
          });
          points.push({
            latitude: toDeg(midLat),
            longitude: -180
          });
        }
      }

      points.push({
        latitude: toDeg(lat),
        longitude: toDeg(lon)
      });
      lastLon = lon;
    }

    return points;
  }



  // Calculate Rhumb Line points from New York to London
  var newYork = {
    latitude: 40.7128,
    longitude: -74.0060
  };
  var london = {
    latitude: 51.5074,
    longitude: -0.1278
  };
  var rhumbLinePoints = calculateRhumbLinePoints(newYork, london);

  // Create a line for each pair of points
  rhumbLinePoints.forEach(function(point, index) {
    if (index < rhumbLinePoints.length - 1) {
      lineSeries.pushDataItem({
        geometry: {
          type: "LineString",
          coordinates: [
            [point.longitude, point.latitude],
            [rhumbLinePoints[index + 1].longitude, rhumbLinePoints[index + 1].latitude]
          ]
        }
      });
    }
  });

  // Expose the functions to the global scope so they can be called from the HTML
  window.setProjection = setProjection;
  window.setIndex = setIndex;

}); // end am5.ready()
.tools {
  text-align: center;
}

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

#chartdiv {
  width: 100%;
  height: 500px;
}
<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>
kikon
  • 3,670
  • 3
  • 5
  • 20
Zarty
  • 11
  • 2

0 Answers0