【问题标题】:Draw small symmetrical arc line path between two markers on world map in Highmaps在Highmaps的世界地图上的两个标记之间绘制小的对称弧线路径
【发布时间】:2020-05-04 04:13:02
【问题描述】:

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

my jsfiddle,我只是从演示修改为以下内容:

HTML

<!-- line 5 -->
<script src="https://code.highcharts.com/mapdata/custom/world.js"></script>

JavaScript

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

看看 before 是 Highcharts 演示和 after 是我上面提到的一些更改之间的区别:

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

// 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;
}

如果我将函数修改为始终除以2 来获得弧点xy,那么我会在标记之间得到一条直线:

var arcPointX = (from.x + to.x) / 2,
    arcPointY = (from.y + to.y) / 2;

我不确定如何获得更小、更不夸张的曲线。

按照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>

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

【问题讨论】:

    标签: javascript math svg highcharts


    【解决方案1】:

    您只需让分母更接近 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;
    }
    

    更新:

    我试图只专注于数学: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;
    }
    

    更新 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 设置为 undefined 时,数学就不起作用了。所以现在,虽然斜率为零,但它大于 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); 这基本上意味着将正向或负向远离中心点,幅度等于平方的两倍点之间距离的根。我确定了点之间距离的平方根的两倍,因为这为我们提供了弧的 offsetCenter,它为所有短线和长线提供了类似的柔和曲线。

    现在,让我们考虑一下 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;
      }
    

    更新 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;
     }
    

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

    这也给出了最终结果:

    【讨论】:

    • 我确实尝试过,但我没有坚持下去的原因是因为没有一条线是一致的。例如,London - Bristol 线的曲线比几乎笔直的Belfast - Lerwick 高得多。我不认为魔术数字会是答案,也许我错了?
    • 啊,好吧,我明白你想做什么了。所以我应该计算线的中点(两个机场之间),然后使弧点(曲线的点)稍微远离这个,这样每条线都有一个同样漂亮的“软”曲线。让我试着弄清楚数学。
    • 是的,就是这样;我把它描述为对称的。两点之间每条线的一致软曲线(即机场之间的飞行路径)。请记住,这需要根据我的 jsfiddle jsfiddle.net/chickensoup64/qLuv6bra 扩大到全球地图 - 我也被困在数学上,所以我很想看看你想出什么。感谢您的时间和想法。
    • 解释太棒了!您的代码改进现在非常好,我建议的唯一更改是使用constlet 而不是var,但这不是必需的。对不会更改的值使用const,如果需要更新值,使用let。例如。 const invSlope = -1 / slope;let arcPointX = 0;。很棒的作品!
    • 谢谢 Ash,我现在还想出了如何使用三角函数来删除 if/else 控制流/switch 语句,我认为这更干净、更简单且在数学上更有效:) - 感谢您的提示关于 Const & Let :)
    猜你喜欢
    • 2019-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-05
    • 2016-10-16
    • 1970-01-01
    • 2015-09-19
    • 1970-01-01
    相关资源
    最近更新 更多