在 Highmaps 中的世界地图上的两个标记之间绘制对称的小弧线路径

Ash*_*Ash 1 javascript math svg highcharts

我正在使用 Highmaps 创建一个飞行路径图,使用他们的演示简单飞行路线( jsfiddle ) 作为起点。当我更新代码以使用世界地图时,位置/标记之间的线路径会因夸张的曲线而扭曲。

请参阅我的 jsfiddle,其中我仅将演示修改为以下内容:

HTML

<!-- line 5 -->
<script src="https://code.highcharts.com/mapdata/custom/world.js"></script>
Run Code Online (Sandbox Code Playgroud)

JavaScript

// line 39
mapData: Highcharts.maps['custom/world'],
// line 47
data: Highcharts.geojson(Highcharts.maps['custom/world'], 'mapline'),
Run Code Online (Sandbox Code Playgroud)

查看两者之间的区别,其中之前是 Highcharts 演示,之后是我上面提到的一些更改:

地图线路径比较

路径是通过函数计算的,该函数pointsToPath使用QSVG 中的二次贝塞尔曲线来弯曲标记之间绘制的线。

// Function to return an SVG path between two points, with an arc
function pointsToPath(from, to, invertArc) {
    var arcPointX = (from.x + to.x) / (invertArc ? 2.4 : 1.6),
        arcPointY = (from.y + to.y) / (invertArc ? 2.4 : 1.6);
    return 'M' + from.x + ',' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
            ',' + to.x + ' ' + to.y;
}
Run Code Online (Sandbox Code Playgroud)

如果我修改函数以始终除以2圆弧点xy然后在标记之间得到一条直线:

var arcPointX = (from.x + to.x) / 2,
    arcPointY = (from.y + to.y) / 2;
Run Code Online (Sandbox Code Playgroud)

我不确定获得更小、更不夸张的曲线的数学方法。

理想情况下,我希望根据MDN-Paths 中的示例,该线是对称的:

带网格的二次贝塞尔曲线

<svg width="190" height="160" xmlns="http://www.w3.org/2000/svg">
  <path d="M 10 80 Q 95 10 180 80" stroke="black" fill="transparent"/>
</svg>
Run Code Online (Sandbox Code Playgroud)

使用世界地图数据,如何计算标记之间的线路径以显示更小或对称的曲线?

Ale*_*x L 6

你只需要让你的分母更接近 2.0,因为当它是 2.0 是一条完美的直线:https : //jsfiddle.net/my7bx50p/1/

所以我选择了 2.03 和 1.97,这给了你很多“柔和”的曲线。希望有帮助。

function pointsToPath(from, to, invertArc) {
    var arcPointX = (from.x + to.x) / (invertArc ? 2.03 : 1.97),
        arcPointY = (from.y + to.y) / (invertArc ? 2.03 : 1.97);
    return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY + ' ' + to.x + ' ' + to.y;
}
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

更新:

我试图只关注数学:https : //jsfiddle.net/9gkvhfuL/1/

我认为数学现在是正确的:

在此处输入图片说明

回到真实的例子:https : //jsfiddle.net/my7bx50p/6/

我相信,给出了预期的结果:):

在此处输入图片说明

从代码(https://jsfiddle.net/my7bx50p/6/):

  function pointsToPath(from, to, invertArc) {
var centerPoint = [ (from.x + to.x) / 2, (from.y + to.y) / 2];
var slope = (to.x - from.x) / (to.y - from.y);
var invSlope = -1 / slope;
var distance = Math.sqrt( Math.pow((to.x - from.x), 2) + Math.pow((to.y - from.y), 2) );

if (Math.abs(slope) > Math.abs(invSlope) ){
  //then we should offset in the y direction
  var offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
  var min_slope = Math.min( Math.abs(slope), Math.abs(invSlope) );
  var final_slope = Math.max(min_slope, 1);
  var offsetCenter = [centerPoint[0] + (offset * (1/slope)), centerPoint[1] + offset];
  //console.log(centerPoint, slope, invSlope, distance);
  var arcPointX = offsetCenter[0], //(from.x + to.x) / (invertArc ? 2.03 : 1.97),
      arcPointY = offsetCenter[1] //(from.y + to.y) / (invertArc ? 2.03 : 1.97);
} else{ //invSlope <= slope
  //then we should offset in the x direction
  var offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
  var min_slope = Math.min( Math.abs(slope), Math.abs(invSlope) );
  var final_slope = Math.max(min_slope, 1);
  var offsetCenter = [centerPoint[0] + offset, centerPoint[1] + (offset * (1/invSlope))];
  //console.log(centerPoint, slope, invSlope, distance);
  var arcPointX = offsetCenter[0], //(from.x + to.x) / (invertArc ? 2.03 : 1.97),
      arcPointY = offsetCenter[1] //(from.y + to.y) / (invertArc ? 2.03 : 1.97);
}   
return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
        ' ' + to.x + ' ' + to.y;
}
Run Code Online (Sandbox Code Playgroud)

更新2:(尝试解释数学并清理代码)

查看数学小提琴:https : //jsfiddle.net/alexander_L/dcormfxy/53/

在此处输入图片说明

两点之间的黑色实线就是它们之间的直线,也有对应的斜率(后面代码中用到)。我还在每条线上画了一个中心点。然后我将反斜率绘制为虚线(也在代码中使用) 反斜率定义为垂直于斜率,并与invSlope = -1/slope. 从这里,我们现在设置找到中心点左侧或右侧的垂直点,这将成为我们对称弧的中心。为此,我们首先要确定斜率是否大于反斜率或反斜率是否大于斜率(绝对值)。这只是必要的,因为当我们有一条完美的水平线或完美的垂直线时,斜率分别为零和未定义,然后我们的数学就不起作用了。(记住斜率 = (y2 - y1)/(x2 - x1) 所以当这条线是垂直的 y 变化但 x 不是这样 x2 = x1 然后分母为零并给我们未定义的斜率)

让我们考虑一下 C 线 from : {x: 40, y: 40}, to : {x: 220, y: 40}

斜率 = (y2 - y1)/(x2 - x1)

斜率 = (40 - 40)/(220 - 40)

斜率 = 0 / 180

斜率 = 0

invSlope = -1/斜率

invSlope = 未定义

这就是为什么我们需要在代码中包含两种情况(if else)的原因,因为每当我们得到斜率或 invSlope 未定义时,数学就不会起作用。所以现在,虽然斜率为零,但它大于 invSlope(未定义)。(注意 SVG 与普通图表相比是颠倒的,以及我们如何看待它们,因此需要您的大脑牢记这一点,否则很容易迷失方向)

所以现在我们可以在 y 方向偏移中心点,然后计算在 x 方向上需要偏移多少。如果您有一条斜率为 1 的直线,那么您将在 x 和 y 方向上偏移相同的值,因为直线的斜率为 1(直线与 x 轴成 45 度角),因此垂直移开仅通过在 x 方向上移动例如 5 和在 y 方向上移动 -5 来实现从那条线。

幸运的是,在这种边缘情况下(斜率 = 0),我们只在 y 方向移动,x 方向偏移 = 0。看看数学示例中的 C 线,你可以明白我的意思,垂直移动,我们只是从中心点向正或负 y 方向移动。从代码:

offsetCenter = [centerPoint[0] + (offset * (1/slope)), centerPoint[1] + offset];

正如我所说,我们在 y 方向上从 centerPoint 偏移,+ (offset * (1/slope))这里的项将为零,因为 1/slope 未定义。我们可以选择通过invertArc在这一行中使用的函数参数来偏移“左”或“右” :var offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);这基本上意味着将正方向或负方向从 centerPoint 移开,幅度等于两者之间距离的平方根的两倍点。我选择了点之间距离平方根的两倍,因为这为我们提供了弧的偏移中心,为所有短线和长线提供了类似的软曲线。

现在,让我们考虑线 A from : {x: 40, y: 40}, to : {x: 320, y: 360}

斜率 = (y2 - y1)/(x2 - x1)

斜率 = (360 - 40)/(320 - 40)

斜率 = 320 / 280

斜率 = 1.143

invSlope = -1/斜率

invSlope = -0.875

最终清理代码和真实示例在这里https://jsfiddle.net/alexander_L/o43ka9u5/4/

  function pointsToPath(from, to, invertArc) {
  var centerPoint = [ (from.x + to.x) / 2, (from.y + to.y) / 2];
  var slope = (to.x - from.x) / (to.y - from.y);
  var invSlope = -1 / slope;
  var distance = Math.sqrt( Math.pow((to.x - from.x), 2) + Math.pow((to.y - from.y), 2) );
  var arcPointX = 0;
  var arcPointY = 0;
  var offset = 0;
  var offsetCenter = 0;

  if (Math.abs(slope) > Math.abs(invSlope) ){
    //then we should offset in the y direction (then calc. x-offset)
    offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);

    offsetCenter = [centerPoint[0] + (offset * (1/slope)), centerPoint[1] + offset];

    arcPointX = offsetCenter[0] 
    arcPointY = offsetCenter[1]
  } else{ //invSlope >= slope
    //then we should offset in the x direction (then calc. y-offset)
    offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
    offsetCenter = [centerPoint[0] + offset, centerPoint[1] + (offset * (1/invSlope))];

    arcPointX = offsetCenter[0] 
    arcPointY = offsetCenter[1] 
  }   
  return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
          ' ' + to.x + ' ' + to.y;
  }
Run Code Online (Sandbox Code Playgroud)

更新 3:

我想出了如何通过使用三角函数来消除对 if else 控制流/ switch 语句的需要。我希望我的草图有助于解释逻辑,你可能还想阅读一些东西(https://study.com/academy/lesson/sohcahtoa-definition-example-problems-quiz.html)等,因为我很难解释在这里简要介绍一下(我已经在这里写了一篇文章:) 所以不会解释 SOH CAH TOA 等)

在此处输入图片说明

这使得核心函数代码如下(仅数学 - https://jsfiddle.net/alexander_L/dcormfxy/107/)(完整示例 - https://jsfiddle.net/alexander_L/o43ka9u5/6/):

function pointsToPath(from, to, invertArc) {
  const centerPoint = [ (from.x + to.x) / 2, (from.y + to.y) / 2];
  const slope = (to.y - from.y) / (to.x - from.x);
  const invSlope = -1 / slope;
  const distance = Math.sqrt( Math.pow((to.x - from.x), 2) + Math.pow((to.y - from.y), 2) );
  const offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);

  const angle = Math.atan(slope);
  //Math.cos(angle) = offsetY/offset;
  //Math.sin(angle) = offsetX/offset;
  const offsetY = Math.cos(angle)*offset;
  const offsetX = Math.sin(angle)*offset;
  //if slope = 0 then effectively only offset y-direction
  const offsetCenter = [centerPoint[0] - offsetX, centerPoint[1] + offsetY];
  const arcPointX = offsetCenter[0]
  const arcPointY = offsetCenter[1]   
  return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
          ' ' + to.x + ' ' + to.y;
 }
Run Code Online (Sandbox Code Playgroud)

我相信这段代码更优雅、更简洁、更健壮,并且在数学上更有效:) 也感谢 Ash 关于 Const & Let use 与 var 的提示。

这也给出了最终结果:

在此处输入图片说明

在此处输入图片说明