Решение для маркировки маршрута или пути с поворотами - PullRequest
0 голосов
/ 30 октября 2018

Я хочу предоставить своим пользователям простой способ визуально отследить маршрут на карте или картинке. Решение должно позволять пользователям добавлять контрольные точки, которые они могут использовать для создания изгибов на маршруте.

Это должно работать с html5 canvas - в настоящее время я использую библиотеку Konvajs, так что решение, которое использует это, было бы хорошо.

В интересах совместного использования и изучения, если вы можете предложить решения, использующие другие библиотеки HTML5-холста, которые тоже было бы неплохо увидеть.

Примечание. Это не первоначальный вопрос. Однако со временем выяснилось, что это было фактическим требованием. ОП запросил средства для нахождения произвольной точки на некотором пути вдоль линии / кривой на холсте HTML5, чтобы можно было добавить перетаскиваемую контрольную точку в этой точке для редактирования линии / кривой. Принятый ответ не отвечает этой потребности. Тем не менее, ответ на этот оригинальный вопрос будет включать серьезную математику обнаружения столкновений и потенциальное использование контрольных точек Безье - другими словами, это будет большой вопрос, в то время как принятый ответ является очень доступным решением с непротиворечивым UX.

Исходный вопрос можно увидеть по ссылкам редактирования ниже этого вопроса.

1 Ответ

0 голосов
/ 01 ноября 2018

Как насчет этой идеи. Вы щелкаете в том месте, где хотите получить следующую точку, и линия маршрута проходит с новыми маркерами позиционирования вдоль отрезков линии. Если вам нужны стрелки, вы можете расширить объекты здесь, как вам нужно. Вы можете легко изменить цвета, ширину обводки, непрозрачность круга и т. Д. С помощью атрибутов класса маршрута. Точки доступны в массиве и в стандартном списке точек линии Konva.js. JS ванильный, другие библиотеки не нужны и не используются.

Кнопка «Экспорт» показывает, как получить объекты (x, y) с фиксированной точкой для экспорта.

Пример видео здесь, рабочий код приведен ниже.

enter image description here

// 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>
...