เหตุการณ์ D3 Click ไม่ทำงานหลังจากต่อท้ายเส้นทางอีกครั้ง

Jan 03 2021

ฉันพยายามสร้างโลกที่ลากได้นี้ขึ้นมาใหม่จาก Mike Bostock โดยใช้ D3 แต่เป็นเวอร์ชัน svg เนื่องจากปัญหาด้านประสิทธิภาพขณะลากฉันกำลังเรนเดอร์โลก จนถึงตอนนี้ดีมาก ตอนนี้ฉันต้องการใช้เหตุการณ์การคลิก แต่ไม่ได้ผล นี่มันบอกว่าอาจจะมีปัญหาอีกครั้งท้าย เหตุการณ์ที่เลื่อนลงทำงานได้ดี แต่จะรบกวนการลากในภายหลัง เหตุใดเหตุการณ์ mousedown จึงทำงานและเหตุการณ์คลิกไม่ได้ คำแนะนำในการปรับโครงสร้างโค้ดเพื่อแก้ปัญหานี้ได้รับการชื่นชมอย่างมาก

ฉันสร้าง Fiddle เพื่อความเข้าใจที่ดีขึ้น: Fiddle

ปล. ฉันยังใหม่กับการเขียนโปรแกรมและ D3 ดังนั้นโปรดอย่ารุนแรงเกินไป :)

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)
}

คำตอบ

1 AndrewReid Jan 03 2021 at 08:51

คุณจะไม่ทำให้เกิดการคลิกอย่างที่เป็นอยู่ในขณะนี้ซึ่งคุณเคยเห็น เนื่องจากการคลิกเกี่ยวข้องกับทั้งเมาส์ลงและเลื่อนเมาส์ขึ้น สิ่งเหล่านี้โต้ตอบกับพฤติกรรมการลากโดยแต่ละเหตุการณ์จะทริกเกอร์เหตุการณ์เริ่มต้นและสิ้นสุดตามลำดับ

สิ่งที่เกิดขึ้นในกรณีของคุณคือมูสดาวน์ทริกเกอร์การลากและตัวฟังมูสดาวน์ที่คุณเพิ่มลงในพา ธ จากนั้นเมื่อเลื่อนเมาส์ขึ้นตัวฟังเหตุการณ์การลากจะเริ่มทำงานก่อนโดยลบเส้นทางและตัวฟังที่เกี่ยวข้องออก ฟังก์ชั่นการแสดงผลจะเพิ่มเส้นทางใหม่หลังจากข้อเท็จจริงสายเกินไปที่จะลงทะเบียนเหตุการณ์

มีวิธีแก้ปัญหาหลายวิธี แต่วิธีที่ง่ายที่สุดคือการลบตัวฟังปลายลากที่คุณมีและแทนที่เมื่อเกิดการลากจริงเท่านั้น (ไม่ใช่ในเหตุการณ์เริ่มต้นในเหตุการณ์ลาก):

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))
}

event.on()วิธีการช่วยให้ผู้ฟังจะนำมาใช้เฉพาะสำหรับท่าทางปัจจุบัน ฟังก์ชั่นการลากจะทริกเกอร์เฉพาะเมื่อมีการเคลื่อนไหวระหว่างการเลื่อนเมาส์ลงและการวางเมาส์ดังนั้นตัวฟังจบนี้จะไม่ถูกใช้หากมีการคลิกเพียงครั้งเดียว

นี่เป็นง่ามซอ

มีทางเลือกอื่นในการแก้ปัญหาจำนวนมากวิธีหนึ่งที่นี่ในขณะที่เรียบง่ายอาจไวต่อการเคลื่อนไหวในระหว่างการคลิกเล็กน้อย นี่เป็นพื้นฐานที่เป็นไปได้สำหรับแนวทางอื่นในการแยกความแตกต่างของเหตุการณ์ที่เกี่ยวข้องกับการวางเมาส์ (คลิกและลากจุดสิ้นสุด)