fix(maps): same reconnect/visibility fix for rover and weather hooks
rover_map_hook and weather_map_hook share the same viewport-scoped data flow as the propagation map — they hit the same blank-tile bug after tab hide or socket reconnect. Add reconnected() + visibilitychange handlers and document the requirement in CLAUDE.md so future hooks that push map_bounds don't regress.
This commit is contained in:
parent
53cc2c9121
commit
d64e502908
3 changed files with 56 additions and 2 deletions
|
|
@ -179,6 +179,7 @@ User submits QSO → enqueue_for_qso() → weather/hrrr/terrain/iemre workers
|
|||
- Never use `@apply` in CSS
|
||||
- No inline `<script>` tags; use colocated JS hooks with `.` prefix names
|
||||
- Vendor deps must be imported into `app.js`/`app.css`, no external script `src` or link `href`
|
||||
- LiveView hooks that push `map_bounds` (or any viewport-scoped data request) MUST implement `reconnected()` and a `document.visibilitychange` listener. Server assigns reset on reconnect while the hook keeps its stale client-side grid lookup — Leaflet tiles regenerated outside that extent paint blank. Both callbacks should call `invalidateSize()` + the bounds-push method; clean up the listener in `destroyed()`.
|
||||
|
||||
### Elixir
|
||||
- Write idiomatic Elixir: use pattern matching, pipe operator, and the standard library
|
||||
|
|
|
|||
|
|
@ -30,11 +30,15 @@ interface RoverMapHook extends ViewHook {
|
|||
gridLookup: Map<string, number>
|
||||
colorScale: ColorScaleEntry[]
|
||||
map: L.Map
|
||||
pushBounds: (this: RoverMapHook) => void
|
||||
visibilityHandler: (() => void) | null
|
||||
renderScores(this: RoverMapHook, scores: Score[]): void
|
||||
interpolateScore(this: RoverMapHook, lat: number, lon: number, step: number): number | null
|
||||
scoreColorRGB(this: RoverMapHook, score: number): RGB
|
||||
renderStations(this: RoverMapHook): void
|
||||
fitBounds(this: RoverMapHook): void
|
||||
reconnected(this: RoverMapHook): void
|
||||
destroyed(this: RoverMapHook): void
|
||||
}
|
||||
|
||||
export const RoverMap: RoverMapHook = {
|
||||
|
|
@ -73,7 +77,7 @@ export const RoverMap: RoverMapHook = {
|
|||
}
|
||||
|
||||
// Report bounds so server can send scores for visible area
|
||||
const pushBounds = () => {
|
||||
this.pushBounds = () => {
|
||||
const b = map.getBounds()
|
||||
this.pushEvent("map_bounds", {
|
||||
south: b.getSouth(), north: b.getNorth(),
|
||||
|
|
@ -82,7 +86,7 @@ export const RoverMap: RoverMapHook = {
|
|||
}
|
||||
|
||||
map.on("moveend", () => {
|
||||
pushBounds()
|
||||
this.pushBounds()
|
||||
})
|
||||
|
||||
this.handleEvent("update_scores", ({ scores }: { scores: Score[] }) => {
|
||||
|
|
@ -94,6 +98,31 @@ export const RoverMap: RoverMapHook = {
|
|||
this.renderStations()
|
||||
})
|
||||
|
||||
// Refresh overlay when the tab returns to visibility. Tiles created while
|
||||
// hidden paint blank if the retained gridLookup no longer covers the
|
||||
// current viewport; re-fetching bounds triggers update_scores and a full
|
||||
// overlay rebuild.
|
||||
this.visibilityHandler = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this.map.invalidateSize()
|
||||
this.pushBounds()
|
||||
}
|
||||
}
|
||||
document.addEventListener("visibilitychange", this.visibilityHandler)
|
||||
},
|
||||
|
||||
reconnected(this: RoverMapHook) {
|
||||
if (this.map) {
|
||||
this.map.invalidateSize()
|
||||
this.pushBounds()
|
||||
}
|
||||
},
|
||||
|
||||
destroyed(this: RoverMapHook) {
|
||||
if (this.visibilityHandler) {
|
||||
document.removeEventListener("visibilitychange", this.visibilityHandler)
|
||||
this.visibilityHandler = null
|
||||
}
|
||||
},
|
||||
|
||||
// --- Propagation score overlay (same as main map) ---
|
||||
|
|
|
|||
|
|
@ -91,9 +91,11 @@ interface WeatherMapHook extends ViewHook {
|
|||
playbackTimer: ReturnType<typeof setInterval> | null
|
||||
_escHandler: (e: KeyboardEvent) => void
|
||||
_layerObserver: MutationObserver
|
||||
visibilityHandler: (() => void) | null
|
||||
|
||||
mounted(this: WeatherMapHook): void
|
||||
destroyed(this: WeatherMapHook): void
|
||||
reconnected(this: WeatherMapHook): void
|
||||
sendBounds(this: WeatherMapHook): void
|
||||
positionDetailPanel(this: WeatherMapHook): void
|
||||
buildLookup(this: WeatherMapHook): void
|
||||
|
|
@ -509,6 +511,17 @@ export const WeatherMap: WeatherMapHook = {
|
|||
}
|
||||
})
|
||||
|
||||
// Tab may be hidden while Leaflet drops/creates tiles; retained
|
||||
// gridLookup stops matching the visible viewport and new tiles paint
|
||||
// blank. Re-fetch bounds on return so update_weather rebuilds the layer.
|
||||
this.visibilityHandler = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this.map.invalidateSize()
|
||||
this.sendBounds()
|
||||
}
|
||||
}
|
||||
document.addEventListener("visibilitychange", this.visibilityHandler)
|
||||
|
||||
this._layerObserver = new MutationObserver(() => {
|
||||
const newLayer = this.el.dataset.selectedLayer as LayerId | undefined
|
||||
if (newLayer && newLayer !== this.selectedLayer) {
|
||||
|
|
@ -550,6 +563,17 @@ export const WeatherMap: WeatherMapHook = {
|
|||
if (this._escHandler) document.removeEventListener("keydown", this._escHandler)
|
||||
if (this.radarRefreshTimer) clearInterval(this.radarRefreshTimer)
|
||||
if (this.playbackTimer !== null) clearInterval(this.playbackTimer)
|
||||
if (this.visibilityHandler) {
|
||||
document.removeEventListener("visibilitychange", this.visibilityHandler)
|
||||
this.visibilityHandler = null
|
||||
}
|
||||
},
|
||||
|
||||
reconnected(this: WeatherMapHook) {
|
||||
if (this.map) {
|
||||
this.map.invalidateSize()
|
||||
this.sendBounds()
|
||||
}
|
||||
},
|
||||
|
||||
sendBounds(this: WeatherMapHook) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue