Skip to content

explorer: small camera moves don't update URL (Bug A residual, follow-up to #201) #204

@rdhyee

Description

@rdhyee

Follow-up to #201 / #203. PR #203 fixed the await-blocking case for Bug A but the harness reveals a residual: camera moves below Cesium's percentageChanged threshold do not fire camera.changed, so the URL write is never even scheduled.

Reproducer

tests/playwright/url_roundtrip_investigation.js against the live deploy (post-#203):

Iter Pan/zoom step Status
2 small pan only (Δlat ≈ 0.02°, no zoom) ❌ URL stale
3 small pan + zoom ✅ URL catches up (event fired by zoom)

Iter 2 specifically: Context A's camera reports lat=35.0150 lng=33.7000 but location.href still has the original lat=34.9957 lng=33.6798. Context B opens the URL and lands at the OLD position.

Mechanism

explorer.qmd:2031 sets viewer.camera.percentageChanged = 0.1, which suppresses camera.changed for sub-threshold moves. A small drag-pan at low altitude doesn't cross the threshold, so the event never fires, so the debounced callback (which contains our URL write — fixed by #203 to happen before any awaits) never runs.

Once a subsequent larger camera move fires camera.changed, the URL catches up to the current camera state — including the previously-uncaptured small pan. So the URL is eventually consistent, but a user who pans-then-immediately-copies sees stale state.

Proposed fix

Add a viewer.camera.moveEnd listener as a parallel URL-write trigger. moveEnd fires once per discrete camera move regardless of magnitude (after mouseup on a drag-pan, after the wheel stops on a zoom). It complements camera.changed — the latter still drives the mode-transition / resolution-load work; moveEnd just makes sure the URL captures every settled camera state.

~3 lines:

viewer.camera.moveEnd.addEventListener(() => {
    if (!viewer._suppressHashWrite) {
        history.replaceState(null, '', buildHash(viewer));
    }
});

The _suppressHashWrite gate already exists and handles the hashchange-flight case correctly (so we don't fight back/forward navigation).

Why a separate PR

#203 was already merged and deployed. This residual is small, targeted, and benefits from independent Codex review.

Acceptance

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions