From 3daa602741dc7b3a11275988f7be92916b0b47d4 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 11:47:29 +0000 Subject: [PATCH] One pin click should call the click handler once, not twice The pin click handler was registered on two layers that always overlap: the visible pins and the invisible hitbox drawn over them. A layer-scoped click fires once per registered layer with a feature under the point, and every visible pin has a hitbox twin at its own point -- same source, larger radius, added alongside it so taps are easier to land. So every pin click ran the handler twice, 3ms apart. Measured on the leaf at 1440, on two different pins, both reporting both layers at the click point; exactly twice, never more. That already shipped as a visible bug once: the second call overwrote the legend state the first had saved, so closing a people-group detail reopened a legend the reader had deliberately collapsed. The store is now idempotent, so that particular symptom cannot return, but the duplicate call was the cause and is still here -- anything that counts, toggles, appends or reports from a pin click is wrong by a factor of two, and nothing in the code warns the next author. Bind the click to the hitbox only. It covers every click the visible layer would catch: both layers are created together by addLanguageFamilyLayer, from the same source, and the two places that filter the hitbox mirror the pin filter rather than narrowing it, so the two layers always carry the same features. The handler reads only properties and geometry, which are identical between them. Cursor handlers stay on both layers -- mouseenter and mouseleave are idempotent, so they were never part of this. Checked before relying on it: exactly one addLayer creates each of the two layers, both inside addLanguageFamilyLayer as consecutive statements with no branch between them; no flag gates the hitbox; no removeLayer anywhere touches either; and the only layout writes on the pin layer set circle-sort-key. The invariant is now stated at the binding, because it is not visible from the call site: a pins layer added without a hitbox would get no clicks. Also fixes a dead trap in the same file. The people-groups-pins branch called the handler with no argument, so it fell back to the language-family pins and bound the wrong layer. Nothing creates that layer today -- the id occurs only in this file -- so the branch never runs, but whoever first adds it would have got a duplicate binding, or no click handler at all if the language-family layer were absent. --- library/composables/useMapEvents.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/library/composables/useMapEvents.js b/library/composables/useMapEvents.js index f81a83fea..69c5ecb39 100644 --- a/library/composables/useMapEvents.js +++ b/library/composables/useMapEvents.js @@ -365,9 +365,21 @@ export function useMapEvents(options = {}) { * Attach all event handlers for language family layer */ function attachLanguageFamilyEvents() { - attachPinClickHandler(); - attachCursorHandlers('language-family-pins'); + // ★ CLICKS BIND TO THE HITBOX ONLY, AND THAT IS DELIBERATE. addLanguageFamilyLayer creates + // the hitbox together with the pins, from the same source, with a larger radius and its + // filter mirrored — so it already covers every click the visible layer would catch. A pins + // layer added WITHOUT a hitbox would get no clicks at all. + // + // ⚠ BINDING BOTH IS NOT HARMLESS, IT DOUBLES EVERY CLICK. A layer-scoped click fires once + // per registered layer that has a feature under the point, and every visible pin has a + // hitbox twin at its own point — so registering this handler on both layers called it + // TWICE per click, 3ms apart (measured on the leaf at 1440, two different pins, both + // reporting both layers at the click point). That shipped as a visible bug: the second + // call overwrote the legend state the first had saved, so closing a detail reopened a + // legend the reader had collapsed. Anything that counts, toggles or reports from a pin + // click was also wrong by a factor of two. attachPinClickHandler('language-family-pins-hitbox'); + attachCursorHandlers('language-family-pins'); attachCursorHandlers('language-family-pins-hitbox'); attachClusterClickHandler(); attachCursorHandlers('clusters'); @@ -401,9 +413,16 @@ export function useMapEvents(options = {}) { attachRegionEvents(); } - // People groups pins (alternate layer name) + // People groups pins (alternate layer name). + // + // ⚠ NOTHING CREATES THIS LAYER TODAY, so this branch never runs — the id appears only in + // this file. It is left in place for a map that adds it later, with the argument it was + // missing: the call below used to omit the layer id, which made it fall back to + // 'language-family-pins' (see attachPinClickHandler) and bind the wrong layer. Whoever + // first adds a people-groups-pins layer would have got either a duplicate binding on the + // language-family pins or, with no such layer present, no click handler at all. if (map.getLayer('people-groups-pins')) { - attachPinClickHandler(); // Reuse same popup logic + attachPinClickHandler('people-groups-pins'); attachCursorHandlers('people-groups-pins'); }