export type HydrogenHeatmapMetric = 'kg' | 'refuels' | 'vehicles'; export type HydrogenPayer = 'all' | 'lingniu' | 'customer'; export type HydrogenStationRecord = { station_id: string; station_name: string; address: string; longitude: string | number; latitude: string | number; kg: string | number; refuel_count: string | number; vehicle_count: string | number; first_refuel: string; last_refuel: string; }; type HydrogenHeatmapQueryInput = { startDate?: string; endDate?: string; query?: string; payer?: string; metric?: string; }; type NearbyQueryInput = { lng?: string; lat?: string; radiusKm?: string; }; export const HYDROGEN_HEATMAP_DEFAULT_START = '2026-01-01'; export const HYDROGEN_HEATMAP_DEFAULT_END = '2026-07-13'; function isDate(value: string | undefined): value is string { return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value)); } function parseMetric(value: string | undefined): HydrogenHeatmapMetric { if (value === 'refuels' || value === 'vehicles') return value; return 'kg'; } function parsePayer(value: string | undefined): HydrogenPayer { if (value === 'lingniu' || value === 'customer') return value; return 'all'; } export function parseHydrogenHeatmapQuery(input: HydrogenHeatmapQueryInput) { return { startDate: isDate(input.startDate) ? input.startDate : HYDROGEN_HEATMAP_DEFAULT_START, endDate: isDate(input.endDate) ? input.endDate : HYDROGEN_HEATMAP_DEFAULT_END, query: (input.query || '').trim(), payer: parsePayer(input.payer), metric: parseMetric(input.metric), }; } export function parseNearbyQuery(input: NearbyQueryInput) { const longitude = Number(input.lng); const latitude = Number(input.lat); return { center: Number.isFinite(longitude) && Number.isFinite(latitude) ? { lng: longitude, lat: latitude } : null, radiusKm: Math.min(200, Math.max(5, Number(input.radiusKm) || 50)), }; } export function hydrogenMetricValue( row: HydrogenStationRecord, metric: HydrogenHeatmapMetric, ): number { if (metric === 'refuels') return Number(row.refuel_count) || 0; if (metric === 'vehicles') return Number(row.vehicle_count) || 0; return Number(row.kg) || 0; } export function serializeHydrogenStation( row: HydrogenStationRecord, metric: HydrogenHeatmapMetric, ) { return { stationId: row.station_id, stationName: row.station_name, address: row.address, lng: Number(row.longitude), lat: Number(row.latitude), kg: Number(row.kg) || 0, refuelCount: Number(row.refuel_count) || 0, vehicleCount: Number(row.vehicle_count) || 0, firstRefuel: row.first_refuel, lastRefuel: row.last_refuel, metricValue: hydrogenMetricValue(row, metric), }; } export function rankHydrogenStations( stations: HydrogenStationRecord[], metric: HydrogenHeatmapMetric, ): HydrogenStationRecord[] { return [...stations].sort((left, right) => ( hydrogenMetricValue(right, metric) - hydrogenMetricValue(left, metric) )); } export function buildHydrogenPoints( stations: HydrogenStationRecord[], metric: HydrogenHeatmapMetric, ) { const points = stations.map((row) => ({ lng: Number(row.longitude), lat: Number(row.latitude), count: hydrogenMetricValue(row, metric), })); return { points, max: Math.max(1, ...points.map((point) => point.count)), }; } export function haversineKm( leftLng: number, leftLat: number, rightLng: number, rightLat: number, ): number { const radians = (degrees: number) => degrees * Math.PI / 180; const earthRadius = 6371; const dLat = radians(rightLat - leftLat); const dLng = radians(rightLng - leftLng); const a = Math.sin(dLat / 2) ** 2 + Math.cos(radians(leftLat)) * Math.cos(radians(rightLat)) * Math.sin(dLng / 2) ** 2; return earthRadius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } export function filterHydrogenStationsByRadius( stations: HydrogenStationRecord[], center: { lng: number; lat: number }, radiusKm: number, ): HydrogenStationRecord[] { return stations.filter((row) => haversineKm( center.lng, center.lat, Number(row.longitude), Number(row.latitude), ) <= radiusKm); }