Zdarzenie D3 Click nie działa po ponownym dołączeniu ścieżki
Próbuję odtworzyć tę przeciągalną kulę od Mike'a Bostocka przy użyciu D3, ale jako wersję svg. Ze względu na problemy z wydajnością podczas przeciągania ponownie renderuję kulę ziemską. Jak na razie dobrze. Teraz chcę zaimplementować zdarzenie kliknięcia, ale to nie działa. Tutaj jest wspomniane, że problemem może być ponowne dołączanie. Zdarzenie przesunięcia kursora działa dobrze, ale później przeszkadza w przeciąganiu. Dlaczego zdarzenie mousedown działa, a kliknięcie nie? Bardzo cenione są wskazówki dotyczące restrukturyzacji kodu w celu rozwiązania tego problemu.
Stworzyłem Fiddle dla lepszego zrozumienia: Fiddle
PS. Jestem nowy w programowaniu i D3, więc proszę nie bądź zbyt ostry :)
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)
}
Odpowiedzi
Nigdy nie uruchomisz kliknięcia, jakie jest teraz, które widziałeś. Dzieje się tak, ponieważ kliknięcie obejmuje zarówno naciśnięcie, jak i podniesienie myszy. Te współdziałają z zachowaniem przeciągania, przy czym każde z nich wyzwala odpowiednio zdarzenia początkowe i końcowe.
To, co dzieje się w twoim przypadku, to wyciszanie kursora aktywujące przeciąganie i słuchacz, który dodałeś do ścieżek. Następnie, po przesunięciu wskaźnika myszy w górę, detektor zdarzenia przeciągania uruchamia się jako pierwszy, usuwając wszelkie ścieżki i powiązane nasłuchiwania. Funkcja render dodaje nowe ścieżki po fakcie, zbyt późno, aby zarejestrować zdarzenie.
Istnieje wiele rozwiązań, ale być może najłatwiejszym jest usunięcie posiadanego detektora przeciągania końca i zastąpienie go tylko wtedy, gdy przeciągnięcie faktycznie nastąpi (nie w zdarzeniu początkowym, w zdarzeniu przeciągania):
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()
Metoda pozwala słuchacze mają być stosowane tylko dla bieżącego gestu. Odbiornik przeciągania jest wyzwalany tylko wtedy, gdy między naciśnięciem myszy a przesunięciem myszy znajduje się ruch myszy, więc ten odbiornik końcowy nie będzie używany, jeśli wystarczy jedno kliknięcie.
Oto rozwidlone skrzypce .
Istnieje wiele alternatywnych rozwiązań tego problemu, ale to tutaj, choć proste, może być nieco wrażliwe na ruch podczas kliknięcia. Oto możliwa podstawa do alternatywnego podejścia do różnicowania zdarzeń związanych z przesunięciem myszy (koniec kliknięcia i przeciągnięcia).