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="<" 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=">" 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>