Решение для разметки маршрута или пути с поворотами

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

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

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

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

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


person Huy    schedule 29.10.2018    source источник
comment
Каков пользовательский интерфейс узла - должен ли он стать ручкой для «сгибания» стержня стрелы?   -  person Vanquished Wombat    schedule 30.10.2018
comment
Да. Он должен иметь возможность манипулировать стрелкой, чтобы она могла перемещаться туда, куда пользователь берет ее на карте, например, путем добавления нескольких узлов.   -  person Huy    schedule 31.10.2018
comment
хорошо - так, как будто они показывали маршрут на карте - начать отсюда, повернуть налево здесь ... повернуть направо ... и т. д.? Там, где я вижу, что это делается в других приложениях, пользователь щелкает точку, чтобы сбросить булавку карты (конечную точку), и линия продолжается как прямая линия до этой точки. Тогда есть небольшой «узел» на полпути между последней точкой и булавкой, и пользователь может перетащить узел, чтобы образовать кривую. Эффект состоит в том, чтобы позволить пользователю изгибать линию маршрута вокруг поворотов маршрута и крутых поворотов. Это оно?   -  person Vanquished Wombat    schedule 31.10.2018
comment
Если у вас есть вопрос о добавлении кода, также покажите код, который у вас уже есть (в форме минимального воспроизводимого примера) .   -  person Mike 'Pomax' Kamermans    schedule 31.10.2018
comment
Ах, это могло сработать. Прямо сейчас я щелкаю и перетаскиваю, чтобы создать двухточечную стрелку. Шаг, который я изначально хотел отредактировать, заключался в том, чтобы дважды щелкнуть, чтобы перейти в режим редактирования, чтобы добавить больше узлов. Но в соответствии с предложением, возможно, просто нажмите на холст, чтобы создать точки, когда вы идете, чтобы создать последнюю стрелку, тогда у вас будут редактируемые точки, чтобы поиграть в конце. Невозможно добавить новые точки, но у вас, вероятно, достаточно очков, чтобы поиграть с ними, чтобы внести незначительные изменения.   -  person Huy    schedule 31.10.2018


Ответы (1)


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

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

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

введите здесь описание изображения

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

person Vanquished Wombat    schedule 01.11.2018
comment
Это фантастика! Именно то, что я описал :) спасибо !! Я, вероятно, немного поиграю с кодом и посмотрю, что у меня получится, и извлечу уроки из этого. Спасибо еще раз! - person Huy; 03.11.2018
comment
@ Привет - Спасибо, рад помочь. Пара моментов - во-первых, не могли бы вы отметить мой ответ как правильный, чтобы помочь будущим людям, ищущим помощи, оценить качество, и, во-вторых, не могли бы вы, если бы я отредактировал ваш вопрос, чтобы сделать его немного более репрезентативным для общих требований для ' разметка маршрута или пути на холсте ». Опять же, это для будущих людей, ищущих нечто подобное, которые могут не распознать ваш вопрос как относящийся к той же теме. - person Vanquished Wombat; 03.11.2018
comment
Я пометил это как правильное и никаких проблем с тем, чтобы вы изменили исходный пост. Огромное спасибо! - person Huy; 04.11.2018