| import { getRpcBaseUrl } from '@/services/rpc-client'; |
| import type { GetDisplacementSummaryResponse as ProtoResponse, CountryDisplacement as ProtoCountry, DisplacementFlow as ProtoFlow } from '@/generated/client/worldmonitor/displacement/v1/service_client'; |
| import { createCircuitBreaker, getCSSColor } from '@/utils'; |
| import { DisplacementServiceClient } from '@/services/generated-rpc-clients'; |
| import { publicRpcFetch } from '@/services/public-rpc-fetch'; |
|
|
| |
|
|
| export interface DisplacementFlow { |
| originCode: string; |
| originName: string; |
| asylumCode: string; |
| asylumName: string; |
| refugees: number; |
| originLat?: number; |
| originLon?: number; |
| asylumLat?: number; |
| asylumLon?: number; |
| } |
|
|
| export interface CountryDisplacement { |
| code: string; |
| name: string; |
| refugees: number; |
| asylumSeekers: number; |
| idps: number; |
| stateless: number; |
| totalDisplaced: number; |
| hostRefugees: number; |
| hostAsylumSeekers: number; |
| hostTotal: number; |
| lat?: number; |
| lon?: number; |
| } |
|
|
| export interface UnhcrSummary { |
| year: number; |
| globalTotals: { |
| refugees: number; |
| asylumSeekers: number; |
| idps: number; |
| stateless: number; |
| total: number; |
| }; |
| countries: CountryDisplacement[]; |
| topFlows: DisplacementFlow[]; |
| } |
|
|
| export interface UnhcrFetchResult { |
| ok: boolean; |
| data: UnhcrSummary; |
| cachedAt?: string; |
| } |
|
|
| |
|
|
| const emptyResult: UnhcrSummary = { |
| year: new Date().getFullYear(), |
| globalTotals: { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 }, |
| countries: [], |
| topFlows: [], |
| }; |
|
|
| function toDisplaySummary(proto: ProtoResponse): UnhcrSummary { |
| const s = proto.summary; |
| if (!s) return { ...emptyResult, globalTotals: { ...emptyResult.globalTotals } }; |
|
|
| const gt = s.globalTotals || { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 }; |
| return { |
| year: s.year || new Date().getFullYear(), |
| globalTotals: { |
| refugees: Number(gt.refugees || 0), |
| asylumSeekers: Number(gt.asylumSeekers || 0), |
| idps: Number(gt.idps || 0), |
| stateless: Number(gt.stateless || 0), |
| total: Number(gt.total || 0), |
| }, |
| countries: (s.countries || []).map(toDisplayCountry), |
| topFlows: (s.topFlows || []).map(toDisplayFlow), |
| }; |
| } |
|
|
| function toDisplayCountry(proto: ProtoCountry): CountryDisplacement { |
| return { |
| code: proto.code || '', |
| name: proto.name || '', |
| refugees: Number(proto.refugees || 0), |
| asylumSeekers: Number(proto.asylumSeekers || 0), |
| idps: Number(proto.idps || 0), |
| stateless: Number(proto.stateless || 0), |
| totalDisplaced: Number(proto.totalDisplaced || 0), |
| hostRefugees: Number(proto.hostRefugees || 0), |
| hostAsylumSeekers: Number(proto.hostAsylumSeekers || 0), |
| hostTotal: Number(proto.hostTotal || 0), |
| lat: proto.location?.latitude, |
| lon: proto.location?.longitude, |
| }; |
| } |
|
|
| function toDisplayFlow(proto: ProtoFlow): DisplacementFlow { |
| return { |
| originCode: proto.originCode || '', |
| originName: proto.originName || '', |
| asylumCode: proto.asylumCode || '', |
| asylumName: proto.asylumName || '', |
| refugees: Number(proto.refugees || 0), |
| originLat: proto.originLocation?.latitude, |
| originLon: proto.originLocation?.longitude, |
| asylumLat: proto.asylumLocation?.latitude, |
| asylumLon: proto.asylumLocation?.longitude, |
| }; |
| } |
|
|
| |
|
|
| async function fetchPublicDisplacementSummary(): Promise<ProtoResponse> { |
| return new DisplacementServiceClient(getRpcBaseUrl(), { fetch: publicRpcFetch }) |
| .getDisplacementSummary({ |
| year: 0, |
| countryLimit: 0, |
| flowLimit: 50, |
| }); |
| } |
|
|
| const breaker = createCircuitBreaker<UnhcrSummary>({ |
| name: 'UNHCR Displacement', |
| cacheTtlMs: 10 * 60 * 1000, |
| persistCache: true, |
| }); |
|
|
| |
|
|
| export async function fetchUnhcrPopulation(): Promise<UnhcrFetchResult> { |
| const data = await breaker.execute(async () => { |
| const response = await fetchPublicDisplacementSummary(); |
| return toDisplaySummary(response); |
| }, emptyResult, { shouldCache: (r) => r.countries.length > 0 }); |
|
|
| return { |
| ok: data !== emptyResult && data.countries.length > 0, |
| data, |
| }; |
| } |
|
|
| |
|
|
| export function getDisplacementColor(totalDisplaced: number): [number, number, number, number] { |
| if (totalDisplaced >= 1_000_000) return [255, 50, 50, 200]; |
| if (totalDisplaced >= 500_000) return [255, 150, 0, 200]; |
| if (totalDisplaced >= 100_000) return [255, 220, 0, 180]; |
| return [100, 200, 100, 150]; |
| } |
|
|
| export function getDisplacementBadge(totalDisplaced: number): { label: string; color: string } { |
| if (totalDisplaced >= 1_000_000) return { label: 'CRISIS', color: getCSSColor('--semantic-critical') }; |
| if (totalDisplaced >= 500_000) return { label: 'HIGH', color: getCSSColor('--semantic-high') }; |
| if (totalDisplaced >= 100_000) return { label: 'ELEVATED', color: getCSSColor('--semantic-elevated') }; |
| return { label: '', color: '' }; |
| } |
|
|
| export function formatPopulation(n: number): string { |
| if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; |
| if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`; |
| return String(n); |
| } |
|
|
| export function getOriginCountries(data: UnhcrSummary): CountryDisplacement[] { |
| return [...data.countries] |
| .filter(c => c.refugees + c.asylumSeekers > 0) |
| .sort((a, b) => (b.refugees + b.asylumSeekers) - (a.refugees + a.asylumSeekers)); |
| } |
|
|
| export function getHostCountries(data: UnhcrSummary): CountryDisplacement[] { |
| return [...data.countries] |
| .filter(c => (c.hostTotal || 0) > 0) |
| .sort((a, b) => (b.hostTotal || 0) - (a.hostTotal || 0)); |
| } |
|
|