使用弯道标记路线或路径的解决方案

Posted

技术标签:

【中文标题】使用弯道标记路线或路径的解决方案【英文标题】:Solution for route or path marking with bends 【发布时间】:2019-04-02 22:46:59 【问题描述】:

我想为我的用户提供一种在地图或图片上直观地追踪路线的简单方法。该解决方案必须让用户添加控制点,他们可以使用这些控制点将弯道放入路线中。

它应该适用于 html5 画布 - 我目前使用 Konvajs 库,因此使用它的解决方案会很好。

为了分享和学习,如果您能建议使用其他 HTML5 画布库的解决方案,也很高兴。

注意:这不是最初提出的问题。然而,随着时间的推移,这是实际的要求。 OP 要求在 HTML5 画布中沿着直线/曲线找到任意点的方法,以便可以在该点添加可拖动的控制点来编辑直线/曲线。接受的答案不满足此需求。然而,这个原始问题的答案将涉及严重的碰撞检测数学和可能使用贝塞尔控制点 - 换句话说,这将是一个很大的问题,而接受的答案是一个非常平易近人的解决方案,具有一致的用户体验。

可以通过该问题下方的编辑链接查看原始问题。

【问题讨论】:

结的预期用户体验是什么 - 它是否成为“弯曲”箭头棒的手柄? 是的。它是为了能够操纵箭头,以便它可以到达用户在地图上的任何位置,例如通过添加多个结。 好的 - 就像他们在地图上显示路线一样 - 从这里开始,这里左转...那里右转...等等?我在其他应用程序中看到这样做的地方,用户单击一个点以放置一个地图针(终点),并且该线作为一条直线延伸到该点。然后在最后一点和销之间有一个小“结”,用户可以拖动结来制作曲线。效果是让用户围绕路线中的曲线和急转弯弯曲路线线。是这样吗? 如果您对添加代码有任何疑问,请同时显示您已有的代码(以minimal reproducible example 形式)。 啊,可以。现在我单击并拖动以创建一个两点箭头。我最初想编辑它的步骤是双击进入编辑模式以添加更多结。但根据建议,也许只需在画布上单击以创建点,然后创建最终箭头,然后您将有可编辑的点在最后播放。无法添加新点,但无论如何您可能有足够的点来进行小修改。 【参考方案1】:

这个想法怎么样。单击下一个点所需的位置,路线将沿线段延伸,并带有新的定位手柄。如果您需要箭头,您可以根据需要扩展此处的对象。您可以使用路线类的属性轻松更改颜色、笔触宽度、圆形不透明度等。这些点在数组和标准 Konva.js 线点列表中可用。 JS 是 vanilla,不需要或使用其他库。

导出按钮显示如何抓取 (x,y) 定点对象以进行导出。

此处为示例视频,工作代码在 sn-p 下方。

// Set up the canvas / stage
var s1 = new Konva.Stage(container: 'container1', width: 600, height: 300);

// Add a layer for line
var lineLayer = new Konva.Layer(draggable: false);
s1.add(lineLayer);

// Add a layer for drag points
var pointLayer = new Konva.Layer(draggable: false);
s1.add(pointLayer);

// Add a rectangle to layer to catch events. Make it semi-transparent 
var r = new Konva.Rect(x:0, y: 0,  width: 600, height: 300, fill: 'black', opacity: 0.1)
pointLayer.add(r)

// Everything is ready so draw the canvas objects set up so far.
s1.draw()

// generic canvas end



// Class for the draggable point
// Params: route = the parent object, opts = position info, doPush = should we just make it or make it AND store it
var DragPoint = function(route, opts, doPush)
  var route = route;

  this.x = opts.x;
  this.y = opts.y;
  this.fixed = opts.fixed;
  this.id = randId();  // random id.

  if (doPush)  // in some cases we want to create the pt then insert it in the run of the array and not always at the end
    route.pts.push(this);  
  

  // random id generator
  function randId() 
     return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
  

  // mark the pt as fixed - important state, shown by filled point
  this.makeFixed = function()
    this.fixed = true;
    s1.find('#' + this.id)
        .fill(route.fillColor);      
  
  
  this.kill = function()
    s1.find('#' + this.id)
        .remove();        
  
  
  this.draw = function()
    // Add point & pt
    var circleId = this.id;
 
    var pt = new Konva.Circle(
      id: circleId,
      x: this.x, 
      y: this.y, 
      radius: route.pointRadius,
      opacity: route.pointOpacity,
      strokeWidth: 2,
      stroke: route.strokeColor,
      fill: 'transparent',
      draggable: 'true'    
    )
    pt.on('dragstart', function()
        route.drawState = 'dragging';
    )
    pt.on('dragmove', function()
      var pos = this.getPosition();
      route.updatePt(this.id(), pos)
      route.calc(this.id());
      route.draw();
    )
    pt.on('dragend', function()

      route.drawState = 'drawing';
      var pos = this.getPosition();

      route.updatePt(this.getId(), pos);

      route.splitPts(this.getId());
      
      route.draw();
    )

    if (this.fixed)
      this.makeFixed();
    
    
    
    route.ptLayer.add(pt);
    route.draw();

    
  


var Route = function() 

    this.lineLayer = null;
    this.ptLayer = null;
    this.drawState = '';

    this.fillColor = 'Gold';
    this.strokeColor = 'Gold';
    this.pointOpacity = 0.5;
    this.pointRadius = 10;
    this.color = 'LimeGreen';
    this.width = 5;
  
    this.pts = []; // array of dragging points.

    this.startPt = null;
    this.endPt = null;

    // reset the points 
    this.reset = function()
      for (var i = 0; i < this.pts.length; i = i + 1)
        this.pts[i].kill();
      
      this.pts.length = 0;
      this.draw();
    

    // Add a point to the route.
    this.addPt = function(pos, isFixed) 
      
      if (this.drawState === 'dragging')  // do not add a new point because we were just dragging another
        return null;
      
      
      this.startPt = this.startPt || pos;
      this.endPt = pos;

      // create this new pt
      var pt = new DragPoint(this, x: this.endPt.x, y: this.endPt.y, fixed: isFixed, true, "A");
      pt.draw();
      pt.makeFixed(); // always fixed for manual points
      
      // if first point ignore the splitter process
      if (this.pts.length > 0)
        this.splitPts(pt.id, true);
          

      this.startPt = this.endPt; // remember the last point

      this.calc(); // calculate the line points from the array
      this.draw();  // draw the line 
    

  // Position the points.  
  this.calc = function (draggingId)
    draggingId = (typeof draggingId === 'undefined' ? '---' : draggingId); // when dragging an unfilled point we have to override its automatic positioning.

    for (var i = 1; i < this.pts.length - 1; i = i + 1)

      var d2 = this.pts[i];
      if (!d2.fixed && d2.id !== draggingId)      // points that have been split are fixed, points that have not been split are repositioned mid way along their line segment.

        var d1 = this.pts[i - 1];
        var d3 = this.pts[i + 1];
        var pos = this.getHalfwayPt(d1, d3);
        
        d2.x = pos.x;
        d2.y = pos.y;
      
      s1.find('#' + d2.id).position(x: d2.x, y: d2.y); // tell the shape where to go
    
  

  // draw the line
  this.draw = function ()  

    if (this.drawingLine)
      this.drawingLine.remove();
    
    this.drawingLine = this.newLine(); // initial line point
    
    for (var i = 0; i < this.pts.length; i = i + 1)
      this.drawingLine.points(this.drawingLine.points().concat([this.pts[i].x, this.pts[i].y]))
    
    
    this.ptLayer.draw();
    this.lineLayer.draw();
  

  // When dragging we need to update the position of the point
  this.updatePt = function(id, pos)

      for (var i = 0; i < this.pts.length; i = i + 1)
        if (this.pts[i].id === id)

          this.pts[i].x = pos.x;
          this.pts[i].y = pos.y;

          break;
            
      
  

  // Function to add and return a line object. We will extend this line to give the appearance of drawing.
  this.newLine = function()
    var line = new Konva.Line(
        stroke: this.color,
        strokeWidth: this.width,
        lineCap: 'round',
        lineJoin: 'round',
        tension : .1
      );

    this.lineLayer.add(line)
    return line;
    


  // make pts either side of the split
  this.splitPts = function(id, force)
    var idx = -1;
    
    // find the pt in the array
    for (var i = 0; i < this.pts.length; i = i + 1)
      if (this.pts[i].id === id)
        idx = i;

        if (this.pts[i].fixed && !force)
          return null; // we only split once.
        

        //break;
         
    

    // If idx is -1 we did not find the pt id !
    if ( idx === -1)
      return null
    
    else if (idx === 0  )  
      return null
    
    else  // pt not = 0 or max 

      // We are now going to insert a new pt either side of the one we just dragged
      var d1 = this.pts[idx - 1]; // previous pt to the dragged pt
      var d2 = this.pts[idx    ]; // the pt pt
      var d3 = this.pts[idx + 1]; // the next pt after the dragged pt

      d2.makeFixed()// flag this pt as no longer splittable

      // get point midway from prev pt and dragged pt    
      var pos = this.getHalfwayPt(d1, d2);
      var pt = new DragPoint(this, x: pos.x, y: pos.y, foxed: false, false, "C");
      pt.draw();
      this.pts.splice(idx, 0, pt);

      if (d3)
        // get point midway from dragged pt to next     
        pos = this.getHalfwayPt(d2, d3);
        var pt = new DragPoint(this, x: pos.x, y: pos.y, foxed: false, false, "D");
        pt.draw();
        this.pts.splice(idx + 2, 0, pt); // note idx + 2 !

      

      

    
  
  // convert last point array entry to handy x,y object.
  this.getPoint = function(pts)
    return x: pts[pts.length - 2], y: pts[pts.length - 1];
  
  
  this.getHalfwayPt = function(d1, d2)
    var pos = 
          x: d1.x + (d2.x - d1.x)/2, 
          y: d1.y + (d2.y - d1.y)/2
      
    return pos;
  

  this.exportPoints = function()
    var list = [], pt;    
    console.log('pts=' + this.pts.length)
    for (var i = 0; i < this.pts.length; i = i + 1)      
      pt = this.pts[i]
      if (pt.fixed)
        console.log('push ' + i)
        list.push(x: pt.x, y: pt.y);   
         
      
    return list;
  
  


var route = new Route();
route.lineLayer = lineLayer;
route.ptLayer = pointLayer;

route.fillColor = 'AliceBlue'; 
route.strokeColor = 'Red'; 
route.pointOpacity = 0.5;
route.pointRadius = 7;
route.color = '#2982E8'


// Listen for mouse up on the stage to know when to draw points
s1.on('mouseup touchend', function () 

  route.addPt(s1.getPointerPosition(), true);

  
  
);

// jquery is used here simply as a quick means to make the buttons work.

// Controls for points export
$('#export').on('click', function()

  if ($(this).html() === "Hide")
    $(this).html('Export');
    $('#points').hide();
  
  else 
    $(this).html('Hide');
    $('#points')
      .css('display', 'block')
      .val(JSON.stringify(route.exportPoints()));
    

)

// reset button
$('#reset').on('click', function()
  route.reset();
  )
p

  padding: 4px;

#container1

background-image: url('https://i.stack.imgur.com/gADDJ.png');

#ctrl

position: absolute;
z-index: 10;
margin: 0px;
border: 1px solid red;

#points

width: 500px;
height: 100px;
display: none;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.5/konva.min.js"></script>
<p>Click to add a point, click to add another, drag a point to make a bend, etc.
</p>
<div id='ctrl'>
<button id='reset'>Reset</button>
<button id='export'>Export</button>
<textarea id='points'></textarea>
</div>
<div id='container1' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
<div id='img'></div>

【讨论】:

这太棒了!正是我所描述的:)谢谢!我可能会稍微玩一下代码,看看我如何继续学习并从中学习。再次感谢! @Huy - 感谢您的帮助。几点-首先请您将我的答案标记为正确,以帮助未来寻求帮助来判断质量的人,其次您是否介意我编辑您的问题以使其更能代表“的一般要求”在画布上标记路线或路径'。同样,这适用于未来寻找类似问题的人,他们可能无法将您的问题视为同一主题。 我将其标记为正确,您修改原始帖子没有问题。非常感谢!

以上是关于使用弯道标记路线或路径的解决方案的主要内容,如果未能解决你的问题,请参考以下文章

最佳路线(vijos1423)最短路

提示:解决冲突后,标记更正的路径

在地图上绘制路线,具有多个标记,CodeIgniter

当我将一个标记作为角度5中的参数传递时,无法匹配任何路径

创建一个移动标记。机器人操作系统(ROS)

在离子3上绘制两个标记之间的路径