File size: 3,845 Bytes
9d2d895 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | import { getRpcBaseUrl } from '@/services/rpc-client';
import type { FireDetection, FireConfidence, ListFireDetectionsResponse } from '@/generated/client/worldmonitor/wildfire/v1/service_client';
import { createCircuitBreaker } from '@/utils';
import { getHydratedData } from '@/services/bootstrap';
import { WildfireServiceClient } from '@/services/generated-rpc-clients';
import { resolveFireDetectionTotalCount } from './payload';
export type { FireDetection };
export { resolveFireDetectionTotalCount } from './payload';
// -- Types --
export interface FireRegionStats {
region: string;
fires: FireDetection[];
fireCount: number;
totalFrp: number;
highIntensityCount: number;
possibleExplosionCount: number;
}
export interface FetchResult {
regions: Record<string, FireDetection[]>;
totalCount: number;
skipped?: boolean;
reason?: string;
}
export interface MapFire {
lat: number;
lon: number;
brightness: number;
frp: number;
confidence: number;
region: string;
acq_date: string;
daynight: string;
}
// -- Client --
const client = new WildfireServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) });
const breaker = createCircuitBreaker<ListFireDetectionsResponse>({ name: 'Wildfires', cacheTtlMs: 30 * 60 * 1000, persistCache: true });
const emptyFallback: ListFireDetectionsResponse = { fireDetections: [], fetchedAt: 0, dataAvailable: false };
// -- Public API --
export async function fetchAllFires(_days?: number): Promise<FetchResult> {
const hydrated = getHydratedData('wildfires') as ListFireDetectionsResponse | undefined;
const response = (hydrated?.fireDetections?.length ? hydrated : null) ?? await breaker.execute(async () => {
return client.listFireDetections({ start: 0, end: 0, pageSize: 0, cursor: '', neLat: 0, neLon: 0, swLat: 0, swLon: 0 });
}, emptyFallback);
const detections = response.fireDetections;
if (detections.length === 0) {
return { regions: {}, totalCount: 0, skipped: true, reason: 'no_data' };
}
const regions: Record<string, FireDetection[]> = {};
for (const d of detections) {
const r = d.region || 'Unknown';
(regions[r] ??= []).push(d);
}
return { regions, totalCount: resolveFireDetectionTotalCount(response) };
}
export function computeRegionStats(regions: Record<string, FireDetection[]>): FireRegionStats[] {
const stats: FireRegionStats[] = [];
for (const [region, fires] of Object.entries(regions)) {
const highIntensity = fires.filter(
f => f.brightness > 360 && f.confidence === 'FIRE_CONFIDENCE_HIGH',
);
const possibleExplosions = fires.filter(f => f.possibleExplosion);
stats.push({
region,
fires,
fireCount: fires.length,
totalFrp: fires.reduce((sum, f) => sum + (f.frp || 0), 0),
highIntensityCount: highIntensity.length,
possibleExplosionCount: possibleExplosions.length,
});
}
return stats.sort((a, b) => b.fireCount - a.fireCount);
}
export function flattenFires(regions: Record<string, FireDetection[]>): FireDetection[] {
const all: FireDetection[] = [];
for (const fires of Object.values(regions)) {
for (const f of fires) {
all.push(f);
}
}
return all;
}
export function toMapFires(fires: FireDetection[]): MapFire[] {
return fires.map(f => ({
lat: f.location?.latitude ?? 0,
lon: f.location?.longitude ?? 0,
brightness: f.brightness,
frp: f.frp,
confidence: confidenceToNumber(f.confidence),
region: f.region,
acq_date: new Date(f.detectedAt).toISOString().slice(0, 10),
daynight: f.dayNight,
}));
}
function confidenceToNumber(c: FireConfidence): number {
switch (c) {
case 'FIRE_CONFIDENCE_HIGH': return 95;
case 'FIRE_CONFIDENCE_NOMINAL': return 50;
case 'FIRE_CONFIDENCE_LOW': return 20;
default: return 0;
}
}
|