D3 Sự kiện nhấp chuột không hoạt động sau khi nối lại đường dẫn

Jan 03 2021

Tôi đang cố gắng tạo lại quả địa cầu có thể kéo này từ Mike Bostock bằng cách sử dụng D3 nhưng dưới dạng phiên bản svg. Do các vấn đề về hiệu suất trong khi kéo, tôi đang kết xuất hình ảnh địa cầu. Càng xa càng tốt. Bây giờ tôi muốn triển khai một sự kiện nhấp chuột nhưng nó không hoạt động. Ở đây nó được đề cập rằng vấn đề có thể là sự kết hợp lại. Sự kiện tạm dừng hoạt động tốt nhưng ảnh hưởng đến quá trình kéo sau này. Tại sao sự kiện mousedown hoạt động và sự kiện nhấp chuột thì không? Các gợi ý để tái cấu trúc mã để giải quyết vấn đề này được đánh giá cao.

Tôi đã tạo Fiddle để hiểu rõ hơn: Fiddle

Tái bút. Tôi mới làm quen với lập trình và D3, vì vậy xin đừng quá gay gắt :)

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>D3</title>
</head>
<body>

<div id="world"></div>

<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://unpkg.com/topojson-client@2"></script>

</body>
</html>

JS:

let width, height
height = 150
width = 150

const projection = d3.geoOrthographic()
    .scale((height - 10) / 2)
    .translate([100, height / 2])
    .precision(0);
let path = d3.geoPath().projection(projection)

const svg = d3.select("#world")
    .append("svg")

const g = svg.append("g")

d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json").then(data =>  {
  let data1 = data
  renderGlobe(data1);
})


function renderGlobe(world){
  g.call(drag(projection)
   .on("drag.render",  ()=>render(world, true))   
   .on("end.render",  ()=>render(world, false)  ))
   .call( () => render(world, false))
}


function render(world, x){
  if(x){
    variable = "land"
     world = topojson.feature(world, world.objects.land).features;

  }
  else{
    variable = "countries"
    world = topojson.feature(world, world.objects.countries).features;
  }
  g.selectAll("path").remove()

  g.selectAll(`${variable}`) .data(world) .enter().append("path") .attr("class", `${variable}`)
   .attr("d", path)
   // This click event doesn't work
   .on("click",()=>console.log("Do something"))
   // But mousedown event works
   .on("mousedown",()=>console.log("Mousedown event works"))
        
}


function drag(projection){
   var LonLatStart, eulerStart
   function dragstarted(event){
      LonLatStart = projection.invert(d3.pointer(event))
  
      eulerStart = projection.rotate()
}

var LonLatEnd, eulerEnd
function dragged(event){
  LonLatEnd = projection.rotate(eulerStart).invert(d3.pointer(event))
  eulerEnd = getEulerAngles(LonLatStart, eulerStart, LonLatEnd)
 
  projection.rotate(eulerEnd)
  refresh()
}
return drag = d3.drag()
  .on("start", dragstarted)
  .on("drag", dragged)
}

function refresh(){
  svg.selectAll("path").attr("d", path)
}

// Dragging Math

  let cos = Math.cos,
  acos = Math.acos,
  sin = Math.sin,
  asin = Math.asin,
  atan2 = Math.atan2,
  sqrt = Math.sqrt,
  min = Math.min,
  max = Math.max,
  PI = Math.PI,
  radians = PI / 180,
  degrees = 180 / PI;

// a: original vector, b: ending vector
function crossProduct(a, b){
  return [
    a[1] * b[2] - a[2] * b[1],
    a[2] * b[0] - a[0] * b[2], 
    a[0] * b[1] - a[1] * b[0]
  ]
}

function dotProduct(a, b){
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]    
}

function LengthOfVector(c){
    return sqrt(c[0] * c[0] + c[1] * c[1] + c[2] * c[2])
}

function quaternionEulerFormula(a, b){ 
    let rotationAxis = crossProduct(a,b) , normalizationFactor = sqrt(dotProduct(rotationAxis,rotationAxis))
if (!normalizationFactor) return [1, 0, 0, 0]
    let theta = acos(max(-1, min(1, dotProduct(a, b)))) 
return [
  cos(theta / 2), 
  sin(theta / 2) * rotationAxis[2] / normalizationFactor,
  - sin(theta / 2) * rotationAxis[1] / normalizationFactor,       
  sin(theta / 2) * rotationAxis[0] / normalizationFactor
]
}   

// returns unit quaternion from euler angles [λ, φ, γ]
function unitQuaternion(d){
var lambda = d[0] / 2 * radians, cosLambda = cos(lambda), sinLambda = sin(lambda),
  phi = d[1] / 2 * radians, cosPhi = cos(phi), sinPhi = sin(phi),
  gamma = d[2] / 2 * radians, cosGamma = cos(gamma), sinGamma = sin(gamma)

return [
  cosLambda * cosPhi * cosGamma + sinLambda * sinPhi * sinGamma,
  sinLambda * cosPhi * cosGamma - cosLambda * sinPhi * sinGamma,
  cosLambda * sinPhi * cosGamma + sinLambda * cosPhi * sinGamma,
  cosLambda * cosPhi * sinGamma - sinLambda * sinPhi * cosGamma,
]
}

// quaternion multiplication, returns another quaternion which represents the rotation
function quaternionMultiplication(q0 , q1){
return [
q0[0] * q1[0] - q0[1] * q1[1] - q0[2] * q1[2] - q0[3] * q1[3],
q0[0] * q1[1] + q0[1] * q1[0] + q0[2] * q1[3] - q0[3] * q1[2],
q0[0] * q1[2] - q0[1] * q1[3] + q0[2] * q1[0] + q0[3] * q1[1],
q0[0] * q1[3] + q0[1] * q1[2] - q0[2] * q1[1] + q0[3] * q1[0]
]
}

// converts quaternion to euler angles
function quaternion2eulerAngles(q){
return [
  atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees,
  //asin(2 * (q[0] * q[2] - q[3] * q[1])) * degrees,
  asin(max(-1, min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees,
  atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees
]
}


// converts long, lat to cartesian coordinates x,y,z
function lonlat2cartesian(e) {
let l = e[0] * radians, p = e[1] * radians, cp = cos(p);
return [cp * cos(l), cp * sin(l), sin(p)];
};

function getEulerAngles(positionLonLatStart, eulerAnglesStart, positionLonLatEnd){
let v0 = lonlat2cartesian(positionLonLatStart)
let v1 = lonlat2cartesian(positionLonLatEnd)

let quaternionEnd = quaternionMultiplication(unitQuaternion(eulerAnglesStart), quaternionEulerFormula(v0,v1))
return quaternion2eulerAngles(quaternionEnd)
}

Trả lời

1 AndrewReid Jan 03 2021 at 08:51

Bạn sẽ không bao giờ kích hoạt nhấp chuột như ngay bây giờ, mà bạn đã thấy. Điều này là do một cú nhấp chuột bao gồm cả chuột xuống và chuột lên. Những điều này tương tác với hành vi kéo, với mỗi hành vi sẽ kích hoạt các sự kiện bắt đầu và kết thúc tương ứng.

Điều đang xảy ra trong trường hợp của bạn là trình duyệt tạm dừng kích hoạt kéo và trình xử lý chuyển xuống mà bạn đã thêm vào các đường dẫn. Sau đó khi di chuột lên, trình nghe sự kiện kéo sẽ kích hoạt đầu tiên, loại bỏ mọi đường dẫn và trình nghe liên quan. Chức năng kết xuất thêm các đường dẫn mới sau thực tế, quá muộn để đăng ký sự kiện.

Có một số giải pháp, nhưng có lẽ dễ nhất là loại bỏ trình nghe cuối kéo mà bạn có và chỉ thay thế nó khi một thao tác kéo thực sự xảy ra (không phải trong sự kiện bắt đầu, trong sự kiện kéo):

function renderGlobe(world){
   g.call(drag(projection)
     .on("drag.render",  function(event) {
        render(world, true) 
       event.on("end.render",()=>render(world,false))
   }))
  .call( () => render(world, false))
}

Các event.on()phương pháp cho phép người nghe để được áp dụng chỉ dành cho các cử chỉ hiện tại. Trình nghe kéo chỉ kích hoạt nếu có sự di chuyển chuột giữa mousedown và mouseup, vì vậy trình nghe cuối này sẽ không được sử dụng nếu chỉ có một cú nhấp chuột đơn giản.

Đây là một trò chơi fiddle đã được phân nhánh .

Có một số giải pháp thay thế hợp lý cho vấn đề này, giải pháp ở đây, mặc dù đơn giản, nhưng có thể hơi nhạy cảm với chuyển động trong khi nhấp chuột. Đây là cơ sở có thể có cho một cách tiếp cận thay thế để phân biệt các sự kiện liên quan đến mouseup (nhấp và kéo kết thúc).