feat(radar): only show lightning strikes within 25 miles
Add haversineKm helper and filterStrikesWithinKm filter in src/lib/api/lightning.js. PrecipitationRadar now filters strikes by distance from the current location before projecting them, so only strikes within 25 miles render. No data-source change; relay-down behavior is unchanged. Fixes an incorrect haversine expectation in the test (0.3567 deg of latitude ~= 39.66 km, not 25.7 km).
This commit is contained in:
parent
882fd69a18
commit
86acc56143
@ -2,6 +2,7 @@
|
|||||||
import { app } from '../lib/stores/app.svelte.js'
|
import { app } from '../lib/stores/app.svelte.js'
|
||||||
import { saveSettings } from '../lib/storage/db.js'
|
import { saveSettings } from '../lib/storage/db.js'
|
||||||
import { fetchLightningStrikes, projectStrikeToLayer } from '../lib/api/lightning.js'
|
import { fetchLightningStrikes, projectStrikeToLayer } from '../lib/api/lightning.js'
|
||||||
|
import { filterStrikesWithinKm } from '../lib/api/lightning.js'
|
||||||
|
|
||||||
// --- Radar data ---
|
// --- Radar data ---
|
||||||
let radarData = $state(null)
|
let radarData = $state(null)
|
||||||
@ -30,6 +31,10 @@
|
|||||||
let lightningTimer = null
|
let lightningTimer = null
|
||||||
const LIGHTNING_REFRESH_MS = 30_000
|
const LIGHTNING_REFRESH_MS = 30_000
|
||||||
|
|
||||||
|
// 25 miles is the max distance a lightning strike is worth showing.
|
||||||
|
const MILES_TO_KM = 1.609344
|
||||||
|
const LIGHTNING_VIEW_RADIUS_KM = 25 * MILES_TO_KM
|
||||||
|
|
||||||
// --- Load radar metadata ---
|
// --- Load radar metadata ---
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (loc) {
|
if (loc) {
|
||||||
@ -113,7 +118,7 @@
|
|||||||
|
|
||||||
const strikeMarkers = $derived(
|
const strikeMarkers = $derived(
|
||||||
showLightning
|
showLightning
|
||||||
? strikes.map((s) => ({
|
? filterStrikesWithinKm(strikes, loc.lat, loc.lon, LIGHTNING_VIEW_RADIUS_KM).map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
...projectStrikeToLayer(s.lat, s.lon, { zoom, cx, cy, tileSize: TILE_SIZE }),
|
...projectStrikeToLayer(s.lat, s.lon, { zoom, cx, cy, tileSize: TILE_SIZE }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -102,3 +102,42 @@ function tileY(lat, z) {
|
|||||||
const rad = (lat * Math.PI) / 180
|
const rad = (lat * Math.PI) / 180
|
||||||
return (1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) * Math.pow(2, z - 1)
|
return (1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) * Math.pow(2, z - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Great-circle distance in km between two lat/lon points (Haversine formula).
|
||||||
|
* Used to filter lightning strikes by distance from the current location.
|
||||||
|
*
|
||||||
|
* @param {number} lat1
|
||||||
|
* @param {number} lon1
|
||||||
|
* @param {number} lat2
|
||||||
|
* @param {number} lon2
|
||||||
|
* @returns {number} Distance in kilometers.
|
||||||
|
*/
|
||||||
|
export function haversineKm(lat1, lon1, lat2, lon2) {
|
||||||
|
const R = 6371
|
||||||
|
const toRad = (d) => (d * Math.PI) / 180
|
||||||
|
const dLat = toRad(lat2 - lat1)
|
||||||
|
const dLon = toRad(lon2 - lon1)
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) ** 2 +
|
||||||
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2
|
||||||
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep only the strikes within `radiusKm` of the location. Distances use the
|
||||||
|
* haversine formula so the filter is accurate on the curved surface of the
|
||||||
|
* Earth. Boundary is inclusive (<=).
|
||||||
|
*
|
||||||
|
* @param {Array<{ lat: number, lon: number }>} strikes
|
||||||
|
* @param {number} lat
|
||||||
|
* @param {number} lon
|
||||||
|
* @param {number} radiusKm - max distance to keep, in kilometers.
|
||||||
|
* @returns {Array} Strikes at or inside the radius, preserving order.
|
||||||
|
*/
|
||||||
|
export function filterStrikesWithinKm(strikes, lat, lon, radiusKm) {
|
||||||
|
return strikes.filter((s) => {
|
||||||
|
const d = haversineKm(lat, lon, s.lat, s.lon)
|
||||||
|
return d <= radiusKm
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||||
import { fetchLightningStrikes, projectStrikeToLayer } from '../../../src/lib/api/lightning.js'
|
import { fetchLightningStrikes, projectStrikeToLayer, haversineKm, filterStrikesWithinKm } from '../../../src/lib/api/lightning.js'
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
@ -77,3 +77,54 @@ describe('projectStrikeToLayer', () => {
|
|||||||
expect(b.x).toBeGreaterThan(a.x)
|
expect(b.x).toBeGreaterThan(a.x)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('haversineKm', () => {
|
||||||
|
it('returns ~0 for identical coords', () => {
|
||||||
|
expect(haversineKm(36.35, -95.81, 36.35, -95.81)).toBeCloseTo(0, 5)
|
||||||
|
})
|
||||||
|
it('~39.66km for 0.3567 deg of latitude', () => {
|
||||||
|
expect(haversineKm(36.0, -95.81, 36.3567, -95.81)).toBeCloseTo(39.66, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('~111.19km per 1 deg of latitude', () => {
|
||||||
|
expect(haversineKm(36.0, -95.81, 37.0, -95.81)).toBeCloseTo(111.19, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('filterStrikesWithinKm', () => {
|
||||||
|
// Distances computed with the real haversine helper (not hardcoded), so the
|
||||||
|
// boundary tests are exact regardless of floating-point rounding.
|
||||||
|
const R = haversineKm(36.35, -95.81, 37.0, -95.81)
|
||||||
|
// A lon at 37.0 N whose distance from the location is just past R.
|
||||||
|
let ln = -95.81
|
||||||
|
while (haversineKm(36.35, -95.81, 37.0, ln) <= R) ln += 0.001
|
||||||
|
const pastLon = ln
|
||||||
|
|
||||||
|
const strikes = [
|
||||||
|
{ lat: 36.35, lon: -95.81, time: 1, strength: -10 }, // at the location (0 km) -> keep
|
||||||
|
{ lat: 37.0, lon: -95.81, time: 2, strength: -12 }, // exactly R km north -> keep
|
||||||
|
{ lat: 37.0, lon: pastLon, time: 3, strength: -9 }, // just past R -> drop
|
||||||
|
]
|
||||||
|
|
||||||
|
it('keeps strikes strictly inside the radius', () => {
|
||||||
|
const kept = filterStrikesWithinKm(strikes, 36.35, -95.81, R - 1)
|
||||||
|
expect(kept).toHaveLength(1)
|
||||||
|
expect(kept[0].strength).toBe(-10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps strikes exactly at the radius boundary (inclusive)', () => {
|
||||||
|
const kept = filterStrikesWithinKm(strikes, 36.35, -95.81, R)
|
||||||
|
expect(kept).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops strikes just past the radius', () => {
|
||||||
|
const kept = filterStrikesWithinKm(strikes, 36.35, -95.81, R - 0.0001)
|
||||||
|
expect(kept).toHaveLength(1)
|
||||||
|
expect(kept[0].strength).toBe(-10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns an empty array when everything is beyond the radius', () => {
|
||||||
|
// Reference point far from any strike, with a tiny radius.
|
||||||
|
expect(filterStrikesWithinKm(strikes, 40.0, -95.81, 0.0001)).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user